');\n\n this.destroy = () => {\n $tooltip.remove();\n };\n\n this.findHoverIndexFromDataPoints = (posX: number, series: any, last: number) => {\n const ps = series.datapoints.pointsize;\n const initial = last * ps;\n const len = series.datapoints.points.length;\n let j;\n for (j = initial; j < len; j += ps) {\n // Special case of a non stepped line, highlight the very last point just before a null point\n if (\n (!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) ||\n //normal case\n series.datapoints.points[j] > posX\n ) {\n return Math.max(j - ps, 0) / ps;\n }\n }\n return j / ps - 1;\n };\n\n this.findHoverIndexFromData = (posX: any, series: any) => {\n let lower = 0;\n let upper = series.data.length - 1;\n let middle;\n while (true) {\n if (lower > upper) {\n return Math.max(upper, 0);\n }\n middle = Math.floor((lower + upper) / 2);\n if (series.data[middle][0] === posX) {\n return middle;\n } else if (series.data[middle][0] < posX) {\n lower = middle + 1;\n } else {\n upper = middle - 1;\n }\n }\n };\n\n this.renderAndShow = (absoluteTime: string, innerHtml: string, pos: { pageX: number; pageY: any }, xMode: string) => {\n if (xMode === 'time') {\n innerHtml = '
' + innerHtml;\n }\n $tooltip.html(innerHtml).place_tt(pos.pageX, pos.pageY, { offset: 10 });\n };\n\n this.getMultiSeriesPlotHoverInfo = function (seriesList: any[], pos: { x: number }) {\n let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;\n // 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.\n let results: any = [[], [], []];\n\n //now we know the current X (j) position for X and Y values\n let lastValue = 0; //needed for stacked values\n\n let minDistance, minTime;\n\n for (i = 0; i < seriesList.length; i++) {\n series = seriesList[i];\n\n if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {\n // Init value so that it does not brake series sorting\n results[0].push({ hidden: true, value: 0 });\n continue;\n }\n\n if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {\n // Init value so that it does not brake series sorting\n results[0].push({ hidden: true, value: 0 });\n continue;\n }\n\n if (series.hideTooltip) {\n results[0].push({ hidden: true, value: 0 });\n continue;\n }\n\n hoverIndex = this.findHoverIndexFromData(pos.x, series);\n hoverDistance = pos.x - series.data[hoverIndex][0];\n pointTime = series.data[hoverIndex][0];\n\n // Take the closest point before the cursor, or if it does not exist, the closest after\n if (\n !minDistance ||\n (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||\n (hoverDistance < 0 && hoverDistance > minDistance)\n ) {\n minDistance = hoverDistance;\n minTime = pointTime;\n }\n\n value = series.data[hoverIndex][1];\n\n if (series.stack && value !== null && panel.tooltip.value_type !== 'individual') {\n lastValue += value;\n value = lastValue;\n }\n\n // Highlighting multiple Points depending on the plot type\n if (series.lines.steps || series.stack) {\n // stacked and steppedLine plots can have series with different length.\n // Stacked series can increase its length on each new stacked serie if null points found,\n // to speed the index search we begin always on the last found hoverIndex.\n hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);\n }\n\n // Be sure we have a yaxis so that it does not brake series sorting\n yaxis = 0;\n if (series.yaxis) {\n yaxis = series.yaxis.n;\n }\n\n results[yaxis].push({\n value: value,\n hoverIndex: hoverIndex,\n color: series.color,\n label: series.aliasEscaped,\n time: pointTime,\n distance: hoverDistance,\n index: i,\n });\n }\n\n // Contat the 3 sub-arrays\n results = results[0].concat(results[1], results[2]);\n\n // Time of the point closer to pointer\n results.time = minTime;\n\n return results;\n };\n\n elem.mouseleave(() => {\n if (panel.tooltip?.shared) {\n const plot = elem.data().plot;\n if (plot) {\n $tooltip.detach();\n plot.unhighlight();\n }\n }\n dashboard.events.publish(new LegacyGraphHoverClearEvent());\n dashboard.events.publish(new DataHoverClearEvent());\n });\n\n elem.bind('plothover', (event: any, pos: { panelRelY: number; pageY: number }, item: any) => {\n self.show(pos, item);\n\n // broadcast to other graph panels that we are hovering!\n if (!dashboard.panelInEdit) {\n pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();\n hoverEvent.payload.pos = pos;\n hoverEvent.payload.panel = panel;\n hoverEvent.payload.point['time'] = (pos as any).x;\n dashboard.events.publish(hoverEvent);\n }\n });\n\n elem.bind('plotclick', (event: any, pos: any, item: any) => {\n appEvents.emit(CoreEvents.graphClicked, { pos: pos, panel: panel, item: item });\n });\n\n elem.bind('plotleave', () => {\n if (!panel.tooltip.shared) {\n return;\n }\n\n const plot = elem.data().plot;\n if (plot) {\n $tooltip.detach();\n plot.unhighlight();\n }\n });\n\n this.clear = (plot: { clearCrosshair: () => void; unhighlight: () => void }) => {\n $tooltip.detach();\n plot.clearCrosshair();\n plot.unhighlight();\n };\n\n this.show = (pos: any, item: any) => {\n const plot = elem.data().plot;\n const plotData = plot.getData();\n const xAxes = plot.getXAxes();\n const xMode = xAxes[0].options.mode;\n const seriesList = getSeriesFn();\n let allSeriesMode = panel.tooltip.shared;\n let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;\n\n // if panelRelY is defined another panel wants us to show a tooltip\n // get pageX from position on x axis and pageY from relative position in original panel\n if (pos.panelRelY) {\n const pointOffset = plot.pointOffset({ x: pos.x });\n if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {\n self.clear(plot);\n return;\n }\n\n pos.pageX = elem.offset().left + pointOffset.left;\n pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;\n\n const scrollTop = $(window).scrollTop() ?? 0;\n const isVisible = pos.pageY >= scrollTop && pos.pageY <= $(window).innerHeight()! + scrollTop;\n\n if (!isVisible) {\n self.clear(plot);\n return;\n }\n\n plot.setCrosshair(pos);\n allSeriesMode = true;\n\n if (dashboard.sharedCrosshairModeOnly()) {\n // if only crosshair mode we are done\n return;\n }\n }\n\n if (seriesList.length === 0) {\n return;\n }\n\n if (seriesList[0].hasMsResolution) {\n tooltipFormat = systemDateFormats.fullDateMS;\n } else {\n tooltipFormat = systemDateFormats.fullDate;\n }\n\n if (allSeriesMode) {\n plot.unhighlight();\n\n const seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);\n\n seriesHtml = '';\n\n absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);\n\n // Dynamically reorder the hovercard for the current time point if the\n // option is enabled.\n if (panel.tooltip.sort === 2) {\n seriesHoverInfo.sort((a: { value: number }, b: { value: number }) => {\n return b.value - a.value;\n });\n } else if (panel.tooltip.sort === 1) {\n seriesHoverInfo.sort((a: { value: number }, b: { value: number }) => {\n return a.value - b.value;\n });\n }\n\n for (i = 0; i < seriesHoverInfo.length; i++) {\n hoverInfo = seriesHoverInfo[i];\n\n if (hoverInfo.hidden) {\n continue;\n }\n\n let highlightClass = '';\n if (item && hoverInfo.index === item.seriesIndex) {\n highlightClass = 'graph-tooltip-list-item--highlight';\n }\n\n series = seriesList[hoverInfo.index];\n value = textUtil.sanitize(series.formatValue(hoverInfo.value));\n\n const color = textUtil.sanitize(hoverInfo.color);\n const label = textUtil.sanitize(hoverInfo.label);\n\n seriesHtml +=\n '
';\n plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);\n }\n\n self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);\n } else if (item) {\n // single series tooltip\n const color = textUtil.sanitize(item.series.color);\n series = seriesList[item.seriesIndex];\n group = '
';\n group += ' ' + series.aliasEscaped + ':
';\n\n if (panel.stack && panel.tooltip.value_type === 'individual') {\n value = item.datapoint[1] - item.datapoint[2];\n } else {\n value = item.datapoint[1];\n }\n\n value = textUtil.sanitize(series.formatValue(value));\n absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);\n\n group += '
' + value + '
';\n\n self.renderAndShow(absoluteTime, group, pos, xMode);\n } else {\n // no hit\n $tooltip.detach();\n }\n };\n}\n","import { histogram } from 'd3';\n\nimport TimeSeries from 'app/core/time_series2';\n\n/**\n * Convert series into array of series values.\n * @param data Array of series\n */\nexport function getSeriesValues(dataList: TimeSeries[]): number[] {\n const VALUE_INDEX = 0;\n const values = [];\n\n // Count histogam stats\n for (let i = 0; i < dataList.length; i++) {\n const series = dataList[i];\n const datapoints = series.datapoints;\n for (let j = 0; j < datapoints.length; j++) {\n if (datapoints[j][VALUE_INDEX] !== null) {\n values.push(datapoints[j][VALUE_INDEX]);\n }\n }\n }\n\n return values;\n}\n\n/**\n * Convert array of values into timeseries-like histogram:\n * [[val_1, count_1], [val_2, count_2], ..., [val_n, count_n]]\n * @param values\n * @param bucketSize\n */\nexport function convertValuesToHistogram(values: number[], bucketSize: number, min: number, max: number): any[] {\n const minBound = getBucketBound(min, bucketSize);\n const maxBound = getBucketBound(max, bucketSize);\n\n const histGenerator = histogram()\n .domain([minBound, maxBound])\n .thresholds(Math.round(max - min) / bucketSize);\n\n return histGenerator(values).map((bin) => {\n return [bin.x0, bin.length];\n });\n}\n\n/**\n * Convert series into array of histogram data.\n * @param data Array of series\n * @param bucketSize\n */\nexport function convertToHistogramData(\n data: any,\n bucketSize: number,\n hiddenSeries: any,\n min: number,\n max: number\n): any[] {\n return data.map((series: any) => {\n const values = getSeriesValues([series]);\n series.histogram = true;\n if (!hiddenSeries[series.alias]) {\n const histogram = convertValuesToHistogram(values, bucketSize, min, max);\n series.data = histogram;\n } else {\n series.data = [];\n }\n return series;\n });\n}\n\nfunction getBucketBound(value: number, bucketSize: number): number {\n return Math.floor(value / bucketSize) * bucketSize;\n}\n","import 'vendor/flot/jquery.flot';\nimport $ from 'jquery';\nimport { isNumber } from 'lodash';\n\nimport { PanelCtrl } from 'app/angular/panel/panel_ctrl';\nimport { config } from 'app/core/config';\nimport { CoreEvents } from 'app/types';\n\nexport class ThresholdManager {\n plot: any;\n placeholder: any;\n height: any;\n thresholds: any;\n needsCleanup = false;\n hasSecondYAxis: any;\n\n constructor(private panelCtrl: PanelCtrl) {}\n\n getHandleHtml(handleIndex: any, model: { colorMode: string }, valueStr: any) {\n let stateClass = model.colorMode;\n if (model.colorMode === 'custom') {\n stateClass = 'critical';\n }\n\n return `\n
\n
\n
\n
\n \n ${valueStr}\n
\n
`;\n }\n\n initDragging(evt: any) {\n const handleElem = $(evt.currentTarget).parents('.alert-handle-wrapper');\n const handleIndex = $(evt.currentTarget).data('handleIndex');\n\n let lastY: number | null = null;\n let posTop: number;\n const plot = this.plot;\n const panelCtrl = this.panelCtrl;\n const model = this.thresholds[handleIndex];\n\n function dragging(evt: any) {\n if (lastY === null) {\n lastY = evt.clientY;\n } else {\n const diff = evt.clientY - lastY;\n posTop = posTop + diff;\n lastY = evt.clientY;\n handleElem.css({ top: posTop + diff });\n }\n }\n\n function stopped() {\n // calculate graph level\n let graphValue = plot.c2p({ left: 0, top: posTop }).y;\n graphValue = parseInt(graphValue.toFixed(0), 10);\n model.value = graphValue;\n\n handleElem.off('mousemove', dragging);\n document.removeEventListener('mouseup', stopped);\n\n // trigger digest and render\n panelCtrl.$scope.$apply(() => {\n panelCtrl.render();\n panelCtrl.events.emit(CoreEvents.thresholdChanged, {\n threshold: model,\n handleIndex: handleIndex,\n });\n });\n }\n\n lastY = null;\n posTop = handleElem.position().top;\n\n handleElem.on('mousemove', dragging);\n document.addEventListener('mouseup', stopped);\n }\n\n cleanUp() {\n this.placeholder.find('.alert-handle-wrapper').remove();\n this.needsCleanup = false;\n }\n\n renderHandle(handleIndex: number, defaultHandleTopPos: number) {\n const model = this.thresholds[handleIndex];\n // alerting defines\n if (!model.visible && (this.panelCtrl as any).alert) {\n return;\n }\n\n const value = model.value;\n let valueStr = value;\n let handleTopPos = 0;\n\n // handle no value\n if (!isNumber(value)) {\n valueStr = '';\n handleTopPos = defaultHandleTopPos;\n } else {\n const valueCanvasPos = this.plot.p2c({ x: 0, y: value });\n handleTopPos = Math.round(Math.min(Math.max(valueCanvasPos.top, 0), this.height) - 6);\n }\n\n const handleElem = $(this.getHandleHtml(handleIndex, model, valueStr));\n this.placeholder.append(handleElem);\n\n handleElem.toggleClass('alert-handle-wrapper--no-value', valueStr === '');\n handleElem.css({ top: handleTopPos });\n }\n\n shouldDrawHandles() {\n // @ts-ignore\n return !this.hasSecondYAxis && this.panelCtrl.editingThresholds && this.panelCtrl.panel.thresholds.length > 0;\n }\n\n prepare(elem: JQuery, data: any[]) {\n this.hasSecondYAxis = false;\n for (let i = 0; i < data.length; i++) {\n if (data[i].yaxis > 1) {\n this.hasSecondYAxis = true;\n break;\n }\n }\n\n if (this.shouldDrawHandles()) {\n const thresholdMargin = this.panelCtrl.panel.thresholds.length > 1 ? '220px' : '110px';\n elem.css('margin-right', thresholdMargin);\n } else if (this.needsCleanup) {\n elem.css('margin-right', '0');\n }\n }\n\n draw(plot: any) {\n this.thresholds = this.panelCtrl.panel.thresholds;\n this.plot = plot;\n this.placeholder = plot.getPlaceholder();\n\n if (this.needsCleanup) {\n this.cleanUp();\n }\n\n if (!this.shouldDrawHandles()) {\n return;\n }\n\n this.height = plot.height();\n\n if (this.thresholds.length > 0) {\n this.renderHandle(0, 10);\n }\n if (this.thresholds.length > 1) {\n this.renderHandle(1, this.height - 30);\n }\n\n this.placeholder.off('mousedown', '.alert-handle');\n this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this));\n this.needsCleanup = true;\n }\n\n addFlotOptions(options: any, panel: any) {\n if (!panel.thresholds || panel.thresholds.length === 0) {\n return;\n }\n\n let gtLimit = Infinity;\n let ltLimit = -Infinity;\n let i, threshold, other;\n\n for (i = 0; i < panel.thresholds.length; i++) {\n threshold = panel.thresholds[i];\n if (!isNumber(threshold.value)) {\n continue;\n }\n\n let limit;\n switch (threshold.op) {\n case 'gt': {\n limit = gtLimit;\n // if next threshold is less then op and greater value, then use that as limit\n if (panel.thresholds.length > i + 1) {\n other = panel.thresholds[i + 1];\n if (other.value > threshold.value) {\n limit = other.value;\n ltLimit = limit;\n }\n }\n break;\n }\n case 'lt': {\n limit = ltLimit;\n // if next threshold is less then op and greater value, then use that as limit\n if (panel.thresholds.length > i + 1) {\n other = panel.thresholds[i + 1];\n if (other.value < threshold.value) {\n limit = other.value;\n gtLimit = limit;\n }\n }\n break;\n }\n }\n\n let fillColor, lineColor;\n\n switch (threshold.colorMode) {\n case 'critical': {\n fillColor = 'rgba(234, 112, 112, 0.12)';\n lineColor = 'rgba(237, 46, 24, 0.60)';\n break;\n }\n case 'warning': {\n fillColor = 'rgba(235, 138, 14, 0.12)';\n lineColor = 'rgba(247, 149, 32, 0.60)';\n break;\n }\n case 'ok': {\n fillColor = 'rgba(11, 237, 50, 0.090)';\n lineColor = 'rgba(6,163,69, 0.60)';\n break;\n }\n case 'custom': {\n fillColor = threshold.fillColor;\n lineColor = threshold.lineColor;\n break;\n }\n }\n\n // fill\n if (threshold.fill) {\n if (threshold.yaxis === 'right' && this.hasSecondYAxis) {\n options.grid.markings.push({\n y2axis: { from: threshold.value, to: limit },\n color: config.theme2.visualization.getColorByName(fillColor),\n });\n } else {\n options.grid.markings.push({\n yaxis: { from: threshold.value, to: limit },\n color: config.theme2.visualization.getColorByName(fillColor),\n });\n }\n }\n if (threshold.line) {\n if (threshold.yaxis === 'right' && this.hasSecondYAxis) {\n options.grid.markings.push({\n y2axis: { from: threshold.value, to: threshold.value },\n color: config.theme2.visualization.getColorByName(lineColor),\n });\n } else {\n options.grid.markings.push({\n yaxis: { from: threshold.value, to: threshold.value },\n color: config.theme2.visualization.getColorByName(lineColor),\n });\n }\n }\n }\n }\n}\n","import 'vendor/flot/jquery.flot';\nimport { map } from 'lodash';\n\nimport { dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';\nimport { config } from 'app/core/config';\nimport { calculateTimesWithin, TimeRegionConfig } from 'app/core/utils/timeRegions';\n\ntype TimeRegionColorDefinition = {\n fill: string | null;\n line: string | null;\n};\n\nexport const colorModes: any = {\n gray: {\n themeDependent: true,\n title: 'Gray',\n darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' },\n lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' },\n },\n red: {\n title: 'Red',\n color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' },\n },\n green: {\n title: 'Green',\n color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' },\n },\n blue: {\n title: 'Blue',\n color: { fill: 'rgba(11, 125, 238, 0.12)', line: 'rgba(11, 125, 238, 0.60)' },\n },\n yellow: {\n title: 'Yellow',\n color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' },\n },\n custom: { title: 'Custom' },\n};\n\nexport function getColorModes() {\n return map(Object.keys(colorModes), (key) => {\n return {\n key,\n value: colorModes[key].title,\n };\n });\n}\n\nfunction getColor(timeRegion: any, theme: GrafanaTheme2): TimeRegionColorDefinition {\n if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) {\n timeRegion.colorMode = 'red';\n }\n\n if (timeRegion.colorMode === 'custom') {\n return {\n fill: timeRegion.fill && timeRegion.fillColor ? theme.visualization.getColorByName(timeRegion.fillColor) : null,\n line: timeRegion.line && timeRegion.lineColor ? theme.visualization.getColorByName(timeRegion.lineColor) : null,\n };\n }\n\n const colorMode = colorModes[timeRegion.colorMode];\n\n if (colorMode.themeDependent === true) {\n return theme.isLight ? colorMode.lightColor : colorMode.darkColor;\n }\n\n return {\n fill: timeRegion.fill ? theme.visualization.getColorByName(colorMode.color.fill) : null,\n line: timeRegion.fill ? theme.visualization.getColorByName(colorMode.color.line) : null,\n };\n}\n\ninterface GraphTimeRegionConfig extends TimeRegionConfig {\n colorMode: string;\n\n fill: boolean;\n fillColor: string;\n\n line: boolean;\n lineColor: string;\n}\n\nexport class TimeRegionManager {\n plot: any;\n timeRegions?: TimeRegionConfig[];\n\n constructor(private panelCtrl: any) {}\n\n draw(plot: any) {\n this.timeRegions = this.panelCtrl.panel.timeRegions;\n this.plot = plot;\n }\n\n addFlotOptions(options: any, panel: any) {\n if (!panel.timeRegions?.length) {\n return;\n }\n\n // The panel range\n const tRange: TimeRange = {\n from: dateTime(this.panelCtrl.range.from).utc(),\n to: dateTime(this.panelCtrl.range.to).utc(),\n raw: {\n from: '',\n to: '',\n },\n };\n\n for (const tr of panel.timeRegions) {\n const timeRegion: GraphTimeRegionConfig = tr;\n const regions = calculateTimesWithin(tr, tRange);\n if (regions.length) {\n const timeRegionColor = getColor(timeRegion, config.theme2);\n\n for (let j = 0; j < regions.length; j++) {\n const r = regions[j];\n if (timeRegion.fill) {\n options.grid.markings.push({\n xaxis: { from: r.from, to: r.to },\n color: timeRegionColor.fill,\n });\n }\n\n if (timeRegion.line) {\n options.grid.markings.push({\n xaxis: { from: r.from, to: r.from },\n color: timeRegionColor.line,\n });\n options.grid.markings.push({\n xaxis: { from: r.to, to: r.to },\n color: timeRegionColor.line,\n });\n }\n }\n }\n }\n }\n}\n","import {\n AbsoluteTimeRange,\n DataFrame,\n FieldType,\n LegacyGraphHoverEventPayload,\n reduceField,\n ReducerID,\n dateTimeFormat,\n systemDateFormats,\n} from '@grafana/data';\n\n/**\n * Find the min and max time that covers all data\n */\nexport function getDataTimeRange(frames: DataFrame[]): AbsoluteTimeRange | undefined {\n const range: AbsoluteTimeRange = {\n from: Number.MAX_SAFE_INTEGER,\n to: Number.MIN_SAFE_INTEGER,\n };\n let found = false;\n const reducers = [ReducerID.min, ReducerID.max];\n for (const frame of frames) {\n for (const field of frame.fields) {\n if (field.type === FieldType.time) {\n const calcs = reduceField({ field, reducers });\n range.from = Math.min(range.from, calcs[ReducerID.min]);\n range.to = Math.max(range.to, calcs[ReducerID.max]);\n found = true;\n }\n }\n }\n return found ? range : undefined;\n}\n\n// Check whether event is LegacyGraphHoverEvent\nexport function isLegacyGraphHoverEvent(event: unknown): event is LegacyGraphHoverEventPayload {\n return Boolean(event && typeof event === 'object' && event.hasOwnProperty('pos'));\n}\n\n/** @deprecated */\nexport const graphTickFormatter = (epoch: number, axis: any) => {\n return dateTimeFormat(epoch, {\n format: axis?.options?.timeformat,\n timeZone: axis?.options?.timezone,\n });\n};\n\n/** @deprecated */\nexport const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => {\n if (min && max && ticks) {\n const range = max - min;\n const secPerTick = range / ticks / 1000;\n // Need have 10 millisecond margin on the day range\n // As sometimes last 24 hour dashboard evaluates to more than 86400000\n const oneDay = 86400010;\n const oneYear = 31536000000;\n\n if (secPerTick <= 10) {\n return systemDateFormats.interval.millisecond;\n }\n if (secPerTick <= 45) {\n return systemDateFormats.interval.second;\n }\n if (range <= oneDay) {\n return systemDateFormats.interval.minute;\n }\n if (secPerTick <= 80000) {\n return systemDateFormats.interval.hour;\n }\n if (range <= oneYear) {\n return systemDateFormats.interval.day;\n }\n if (secPerTick <= 31536000) {\n return systemDateFormats.interval.month;\n }\n return systemDateFormats.interval.year;\n }\n\n return systemDateFormats.interval.minute;\n};\n","import 'vendor/flot/jquery.flot';\nimport 'vendor/flot/jquery.flot.selection';\nimport 'vendor/flot/jquery.flot.time';\nimport 'vendor/flot/jquery.flot.stack';\nimport 'vendor/flot/jquery.flot.stackpercent';\nimport 'vendor/flot/jquery.flot.fillbelow';\nimport 'vendor/flot/jquery.flot.crosshair';\nimport 'vendor/flot/jquery.flot.dashes';\nimport './jquery.flot.events';\n\nimport $ from 'jquery';\nimport { clone, find, flatten, isUndefined, map, max as _max, min as _min, sortBy as _sortBy, toNumber } from 'lodash';\nimport React from 'react';\nimport { createRoot, Root } from 'react-dom/client';\n\nimport {\n DataFrame,\n DataFrameView,\n DataHoverClearEvent,\n DataHoverEvent,\n DataHoverPayload,\n DecimalCount,\n FieldDisplay,\n FieldType,\n formattedValueToString,\n getDisplayProcessor,\n getFlotPairsConstant,\n getTimeField,\n getValueFormat,\n hasLinks,\n LegacyEventHandler,\n LegacyGraphHoverClearEvent,\n LegacyGraphHoverEvent,\n LegacyGraphHoverEventPayload,\n LinkModelSupplier,\n PanelEvents,\n toUtc,\n} from '@grafana/data';\nimport { MenuItemProps, MenuItemsGroup } from '@grafana/ui';\nimport { coreModule } from 'app/angular/core_module';\nimport config from 'app/core/config';\nimport { updateLegendValues } from 'app/core/core';\nimport { ContextSrv } from 'app/core/services/context_srv';\nimport { provideTheme } from 'app/core/utils/ConfigProvider';\nimport { tickStep } from 'app/core/utils/ticks';\nimport { TimeSrv } from 'app/features/dashboard/services/TimeSrv';\nimport { DashboardModel } from 'app/features/dashboard/state';\nimport { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';\n\nimport { GraphContextMenuCtrl } from './GraphContextMenuCtrl';\nimport { GraphLegendProps, Legend } from './Legend/Legend';\nimport { alignYLevel } from './align_yaxes';\nimport { EventManager } from './event_manager';\nimport GraphTooltip from './graph_tooltip';\nimport { convertToHistogramData } from './histogram';\nimport { GraphCtrl } from './module';\nimport { ThresholdManager } from './threshold_manager';\nimport { TimeRegionManager } from './time_region_manager';\nimport { isLegacyGraphHoverEvent, graphTickFormatter, graphTimeFormat } from './utils';\n\nconst LegendWithThemeProvider = provideTheme(Legend, config.theme2);\n\nclass GraphElement {\n ctrl: GraphCtrl;\n contextMenu: GraphContextMenuCtrl;\n tooltip: any;\n dashboard: DashboardModel;\n annotations: object[];\n panel: any;\n plot: any;\n sortedSeries?: any[];\n data: any[] = [];\n panelWidth: number;\n eventManager: EventManager;\n thresholdManager: ThresholdManager;\n timeRegionManager: TimeRegionManager;\n declare legendElem: HTMLElement;\n declare legendElemRoot: Root;\n\n constructor(\n private scope: any,\n private elem: JQuery & {\n bind(eventType: string, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; // need to extend with Plot\n },\n private timeSrv: TimeSrv\n ) {\n this.ctrl = scope.ctrl;\n this.contextMenu = scope.ctrl.contextMenuCtrl;\n this.dashboard = this.ctrl.dashboard;\n this.panel = this.ctrl.panel;\n this.annotations = [];\n\n this.panelWidth = 0;\n this.eventManager = new EventManager(this.ctrl);\n this.thresholdManager = new ThresholdManager(this.ctrl);\n this.timeRegionManager = new TimeRegionManager(this.ctrl);\n // @ts-ignore\n this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {\n return this.sortedSeries;\n });\n\n // panel events\n this.ctrl.events.on(PanelEvents.panelTeardown, this.onPanelTeardown.bind(this));\n this.ctrl.events.on(PanelEvents.render, this.onRender.bind(this));\n\n // global events\n // Using old way here to use the scope unsubscribe model as the new $on function does not take scope\n this.ctrl.dashboard.events.on(LegacyGraphHoverEvent.type, this.onGraphHover.bind(this), this.scope);\n this.ctrl.dashboard.events.on(LegacyGraphHoverClearEvent.type, this.onGraphHoverClear.bind(this), this.scope);\n\n this.ctrl.dashboard.events.on(DataHoverEvent.type, this.onGraphHover.bind(this), this.scope);\n this.ctrl.dashboard.events.on(DataHoverClearEvent.type, this.onGraphHoverClear.bind(this), this.scope);\n\n // plot events\n this.elem.bind('plotselected', this.onPlotSelected.bind(this));\n this.elem.bind('plotclick', this.onPlotClick.bind(this));\n\n // get graph legend element\n if (this.elem && this.elem.parent) {\n this.legendElem = this.elem.parent().find('.graph-legend')[0];\n this.legendElemRoot = createRoot(this.legendElem);\n }\n }\n\n onRender(renderData: any[]) {\n this.data = renderData || this.data;\n if (!this.data) {\n return;\n }\n\n this.annotations = this.ctrl.annotations || [];\n this.buildFlotPairs(this.data);\n const graphHeight = this.ctrl.height;\n updateLegendValues(this.data, this.panel, graphHeight);\n\n if (!this.panel.legend.show) {\n if (this.legendElem.hasChildNodes()) {\n this.legendElemRoot.render(null);\n }\n // we need to wait for react to finish rendering the legend before we can render the graph\n // this is a slightly worse version of the `renderCallback` logic we use below\n // the problem here is there's nothing to pass a `renderCallback` to since we don't want to render the legend at all.\n setTimeout(() => {\n this.renderPanel();\n });\n return;\n }\n\n const { values, min, max, avg, current, total } = this.panel.legend;\n const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend;\n const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero };\n const valueOptions = { values, min, max, avg, current, total };\n const legendProps: GraphLegendProps = {\n seriesList: this.data,\n hiddenSeries: this.ctrl.hiddenSeries,\n ...legendOptions,\n ...valueOptions,\n onToggleSeries: this.ctrl.onToggleSeries,\n onToggleSort: this.ctrl.onToggleSort,\n onColorChange: this.ctrl.onColorChange,\n onToggleAxis: this.ctrl.onToggleAxis,\n renderCallback: this.renderPanel.bind(this),\n };\n\n const legendReactElem = React.createElement(LegendWithThemeProvider, legendProps);\n\n // render callback isn't supported in react 18+, see: https://github.com/reactwg/react-18/discussions/5\n this.legendElemRoot.render(legendReactElem);\n }\n\n onGraphHover(evt: LegacyGraphHoverEventPayload | DataHoverPayload) {\n // ignore other graph hover events if shared tooltip is disabled\n if (!this.dashboard.sharedTooltipModeEnabled()) {\n return;\n }\n\n if (isLegacyGraphHoverEvent(evt)) {\n // ignore if we are the emitter\n if (!this.plot || evt.panel?.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) {\n return;\n }\n\n this.tooltip.show(evt.pos);\n }\n\n // DataHoverEvent can come from multiple panels that doesn't include x position\n if (!evt.point?.time) {\n return;\n }\n\n this.tooltip.show({ x: evt.point.time, panelRelY: evt.point.panelRelY ?? 1 });\n }\n\n onPanelTeardown() {\n if (this.plot) {\n this.plot.destroy();\n this.plot = null;\n }\n\n this.tooltip.destroy();\n this.elem.off();\n this.elem.remove();\n\n this.legendElemRoot.unmount();\n }\n\n onGraphHoverClear(handler: LegacyEventHandler
) {\n if (this.plot) {\n this.tooltip.clear(this.plot);\n }\n }\n\n onPlotSelected(event: JQueryEventObject, ranges: any) {\n if (this.panel.xaxis.mode !== 'time') {\n // Skip if panel in histogram or series mode\n this.plot.clearSelection();\n return;\n }\n\n if ((ranges.ctrlKey || ranges.metaKey) && this.dashboard.canAddAnnotations()) {\n // Add annotation\n setTimeout(() => {\n this.eventManager.updateTime(ranges.xaxis);\n }, 100);\n } else {\n this.scope.$apply(() => {\n this.timeSrv.setTime({\n from: toUtc(ranges.xaxis.from),\n to: toUtc(ranges.xaxis.to),\n });\n });\n }\n }\n\n getContextMenuItemsSupplier = (\n flotPosition: { x: number; y: number },\n linksSupplier?: LinkModelSupplier\n ): (() => MenuItemsGroup[]) => {\n return () => {\n // Fixed context menu items\n const items: MenuItemsGroup[] = this.dashboard.canAddAnnotations()\n ? [\n {\n items: [\n {\n label: 'Add annotation',\n ariaLabel: 'Add annotation',\n icon: 'comment-alt',\n onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),\n },\n ],\n },\n ]\n : [];\n\n if (!linksSupplier) {\n return items;\n }\n\n const dataLinks = [\n {\n items: linksSupplier.getLinks(this.panel.replaceVariables).map((link) => {\n return {\n label: link.title,\n ariaLabel: link.title,\n url: link.href,\n target: link.target,\n icon: link.target === '_self' ? 'link' : 'external-link-alt',\n onClick: link.onClick,\n };\n }),\n },\n ];\n\n return [...items, ...dataLinks];\n };\n };\n\n onPlotClick(event: JQueryEventObject, pos: any, item: any) {\n const scrollContextElement = this.elem.closest('.view') ? this.elem.closest('.view').get()[0] : null;\n const contextMenuSourceItem = item;\n\n if (this.panel.xaxis.mode !== 'time') {\n // Skip if panel in histogram or series mode\n return;\n }\n\n if (pos.ctrlKey || pos.metaKey) {\n // Skip if range selected (added in \"plotselected\" event handler)\n if (pos.x !== pos.x1) {\n return;\n }\n\n // skip if dashboard is not saved yet (exists in db) or user cannot edit\n if (!this.dashboard.id || !this.dashboard.canAddAnnotations()) {\n return;\n }\n\n setTimeout(() => {\n this.eventManager.updateTime({ from: pos.x, to: null });\n }, 100);\n return;\n } else {\n this.tooltip.clear(this.plot);\n let linksSupplier: LinkModelSupplier | undefined;\n\n if (item) {\n // pickup y-axis index to know which field's config to apply\n const yAxisConfig = this.panel.yaxes[item.series.yaxis.n === 2 ? 1 : 0];\n const dataFrame = this.ctrl.dataList[item.series.dataFrameIndex];\n const field = dataFrame.fields[item.series.fieldIndex];\n const dataIndex = this.getDataIndexWithNullValuesCorrection(item, dataFrame);\n\n let links: any[] = this.panel.options.dataLinks || [];\n const hasLinksValue = hasLinks(field);\n if (hasLinksValue) {\n // Append the configured links to the panel datalinks\n links = [...links, ...field.config.links!];\n }\n const fieldConfig = {\n decimals: yAxisConfig.decimals,\n links,\n };\n const fieldDisplay = getDisplayProcessor({\n field: { config: fieldConfig, type: FieldType.number },\n theme: config.theme2,\n timeZone: this.dashboard.getTimezone(),\n })(field.values[dataIndex]);\n linksSupplier = links.length\n ? getFieldLinksSupplier({\n display: fieldDisplay,\n name: field.name,\n view: new DataFrameView(dataFrame),\n rowIndex: dataIndex,\n colIndex: item.series.fieldIndex,\n field: fieldConfig,\n hasLinks: hasLinksValue,\n })\n : undefined;\n }\n\n this.scope.$apply(() => {\n // Setting nearest CustomScrollbar element as a scroll context for graph context menu\n this.contextMenu.setScrollContextElement(scrollContextElement);\n this.contextMenu.setSource(contextMenuSourceItem);\n this.contextMenu.setMenuItemsSupplier(this.getContextMenuItemsSupplier(pos, linksSupplier) as any);\n this.contextMenu.toggleMenu(pos);\n });\n }\n }\n\n getDataIndexWithNullValuesCorrection(item: any, dataFrame: DataFrame): number {\n /** This is one added to handle the scenario where we have null values in\n * the time series data and the: \"visualization options -> null value\"\n * set to \"connected\". In this scenario we will get the wrong dataIndex.\n *\n * https://github.com/grafana/grafana/issues/22651\n */\n const { datapoint, dataIndex } = item;\n\n if (!Array.isArray(datapoint) || datapoint.length === 0) {\n return dataIndex;\n }\n\n const ts = datapoint[0];\n const { timeField } = getTimeField(dataFrame);\n\n if (!timeField || !timeField.values) {\n return dataIndex;\n }\n\n const field = timeField.values[dataIndex];\n\n if (field === ts) {\n return dataIndex;\n }\n\n const correctIndex = timeField.values.findIndex((value) => value === ts);\n return correctIndex > -1 ? correctIndex : dataIndex;\n }\n\n shouldAbortRender() {\n if (!this.data) {\n return true;\n }\n\n if (this.panelWidth === 0) {\n return true;\n }\n\n return false;\n }\n\n drawHook(plot: any) {\n // add left axis labels\n if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) {\n $(\"\")\n .text(this.panel.yaxes[0].label)\n .appendTo(this.elem);\n }\n\n // add right axis labels\n if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) {\n $(\"\")\n .text(this.panel.yaxes[1].label)\n .appendTo(this.elem);\n }\n\n const { dataWarning } = this.ctrl;\n if (dataWarning) {\n const msg = $(`${dataWarning.title}
`);\n if (dataWarning.action) {\n $(``)\n .click(dataWarning.action)\n .appendTo(msg);\n }\n msg.appendTo(this.elem);\n }\n this.thresholdManager.draw(plot);\n this.timeRegionManager.draw(plot);\n }\n\n processOffsetHook(plot: any, gridMargin: { left: number; right: number }) {\n const left = this.panel.yaxes[0];\n const right = this.panel.yaxes[1];\n if (left.show && left.label) {\n gridMargin.left = 20;\n }\n if (right.show && right.label) {\n gridMargin.right = 20;\n }\n\n // apply y-axis min/max options\n const yaxis = plot.getYAxes();\n for (let i = 0; i < yaxis.length; i++) {\n const axis: any = yaxis[i];\n const panelOptions = this.panel.yaxes[i];\n axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;\n axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;\n }\n }\n\n processRangeHook(plot: any) {\n const yAxes = plot.getYAxes();\n const align = this.panel.yaxis.align || false;\n\n if (yAxes.length > 1 && align === true) {\n const level = this.panel.yaxis.alignLevel || 0;\n alignYLevel(yAxes, parseFloat(level));\n }\n }\n\n // Series could have different timeSteps,\n // let's find the smallest one so that bars are correctly rendered.\n // In addition, only take series which are rendered as bars for this.\n getMinTimeStepOfSeries(data: any[]) {\n let min = Number.MAX_VALUE;\n\n for (let i = 0; i < data.length; i++) {\n if (!data[i].stats.timeStep) {\n continue;\n }\n if (this.panel.bars) {\n if (data[i].bars && data[i].bars.show === false) {\n continue;\n }\n } else {\n if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {\n continue;\n }\n }\n\n if (data[i].stats.timeStep < min) {\n min = data[i].stats.timeStep;\n }\n }\n\n return min;\n }\n\n // Function for rendering panel\n renderPanel() {\n this.panelWidth = this.elem.width() ?? 0;\n\n if (this.shouldAbortRender()) {\n return;\n }\n\n // give space to alert editing\n this.thresholdManager.prepare(this.elem, this.data);\n\n // un-check dashes if lines are unchecked\n this.panel.dashes = this.panel.lines ? this.panel.dashes : false;\n\n // Populate element\n const options: any = this.buildFlotOptions(this.panel);\n this.prepareXAxis(options, this.panel);\n this.configureYAxisOptions(this.data, options);\n this.thresholdManager.addFlotOptions(options, this.panel);\n this.timeRegionManager.addFlotOptions(options, this.panel);\n this.eventManager.addFlotEvents(this.annotations, options);\n this.sortedSeries = this.sortSeries(this.data, this.panel);\n this.callPlot(options, true);\n }\n\n buildFlotPairs(data: any) {\n for (let i = 0; i < data.length; i++) {\n const series = data[i];\n series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode);\n\n if (series.transform === 'constant') {\n series.data = getFlotPairsConstant(series.data, this.ctrl.range!);\n }\n\n // if hidden remove points and disable stack\n if (this.ctrl.hiddenSeries[series.alias]) {\n series.data = [];\n series.stack = false;\n }\n }\n }\n\n prepareXAxis(options: any, panel: any) {\n switch (panel.xaxis.mode) {\n case 'series': {\n options.series.bars.barWidth = 0.7;\n options.series.bars.align = 'center';\n\n for (let i = 0; i < this.data.length; i++) {\n const series = this.data[i];\n series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];\n }\n\n this.addXSeriesAxis(options);\n break;\n }\n case 'histogram': {\n let bucketSize: number;\n\n if (this.data.length) {\n let histMin = _min(map(this.data, (s) => s.stats.min));\n let histMax = _max(map(this.data, (s) => s.stats.max));\n const ticks = panel.xaxis.buckets || this.panelWidth / 50;\n if (panel.xaxis.min != null) {\n const isInvalidXaxisMin = tickStep(panel.xaxis.min, histMax, ticks) <= 0;\n histMin = isInvalidXaxisMin ? histMin : panel.xaxis.min;\n }\n if (panel.xaxis.max != null) {\n const isInvalidXaxisMax = tickStep(histMin, panel.xaxis.max, ticks) <= 0;\n histMax = isInvalidXaxisMax ? histMax : panel.xaxis.max;\n }\n bucketSize = tickStep(histMin, histMax, ticks);\n options.series.bars.barWidth = bucketSize * 0.8;\n this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);\n } else {\n bucketSize = 0;\n }\n\n this.addXHistogramAxis(options, bucketSize);\n break;\n }\n case 'table': {\n options.series.bars.barWidth = 0.7;\n options.series.bars.align = 'center';\n this.addXTableAxis(options);\n break;\n }\n default: {\n options.series.bars.barWidth = this.getMinTimeStepOfSeries(this.data) / 1.5;\n this.addTimeAxis(options);\n break;\n }\n }\n }\n\n callPlot(options: any, incrementRenderCounter: boolean) {\n try {\n this.plot = $.plot(this.elem, this.sortedSeries, options);\n if (this.ctrl.renderError) {\n delete this.ctrl.error;\n }\n } catch (e) {\n console.error('flotcharts error', e);\n this.ctrl.error = e instanceof Error ? e.message : 'Render Error';\n this.ctrl.renderError = true;\n }\n\n if (incrementRenderCounter) {\n this.ctrl.renderingCompleted();\n }\n }\n\n buildFlotOptions(panel: any) {\n let gridColor = '#c8c8c8';\n if (config.bootData.user.lightTheme === true) {\n gridColor = '#a1a1a1';\n }\n const stack = panel.stack ? true : null;\n const options: any = {\n hooks: {\n draw: [this.drawHook.bind(this)],\n processOffset: [this.processOffsetHook.bind(this)],\n processRange: [this.processRangeHook.bind(this)],\n },\n legend: { show: false },\n series: {\n stackpercent: panel.stack ? panel.percentage : false,\n stack: panel.percentage ? null : stack,\n lines: {\n show: panel.lines,\n zero: false,\n fill: this.translateFillOption(panel.fill),\n fillColor: this.getFillGradient(panel.fillGradient),\n lineWidth: panel.dashes ? 0 : panel.linewidth,\n steps: panel.steppedLine,\n },\n dashes: {\n show: panel.dashes,\n lineWidth: panel.linewidth,\n dashLength: [panel.dashLength, panel.spaceLength],\n },\n bars: {\n show: panel.bars,\n fill: 1,\n barWidth: 1,\n zero: false,\n lineWidth: 0,\n },\n points: {\n show: panel.points,\n fill: 1,\n fillColor: false,\n radius: panel.points ? panel.pointradius : 2,\n },\n shadowSize: 0,\n },\n yaxes: [],\n xaxis: {},\n grid: {\n minBorderMargin: 0,\n markings: [],\n backgroundColor: null,\n borderWidth: 0,\n hoverable: true,\n clickable: true,\n color: gridColor,\n margin: { left: 0, right: 0 },\n labelMarginX: 0,\n mouseActiveRadius: 30,\n },\n selection: {\n mode: 'x',\n color: '#666',\n },\n crosshair: {\n mode: 'x',\n },\n };\n return options;\n }\n\n sortSeries(series: any, panel: any) {\n const sortBy = panel.legend.sort;\n const sortOrder = panel.legend.sortDesc;\n const haveSortBy = sortBy !== null && sortBy !== undefined && panel.legend[sortBy];\n const haveSortOrder = sortOrder !== null && sortOrder !== undefined;\n const shouldSortBy = panel.stack && haveSortBy && haveSortOrder && panel.legend.alignAsTable;\n const sortDesc = panel.legend.sortDesc === true ? -1 : 1;\n\n if (shouldSortBy) {\n return _sortBy(series, (s) => s.stats[sortBy] * sortDesc);\n } else {\n return _sortBy(series, (s) => s.zindex);\n }\n }\n\n getFillGradient(amount: number) {\n if (!amount) {\n return null;\n }\n\n return {\n colors: [{ opacity: 0.0 }, { opacity: amount / 10 }],\n };\n }\n\n translateFillOption(fill: number) {\n if (this.panel.percentage && this.panel.stack) {\n return fill === 0 ? 0.001 : fill / 10;\n } else {\n return fill / 10;\n }\n }\n\n addTimeAxis(options: any) {\n const ticks = this.panelWidth / 100;\n const min = isUndefined(this.ctrl.range!.from) ? null : this.ctrl.range!.from.valueOf();\n const max = isUndefined(this.ctrl.range!.to) ? null : this.ctrl.range!.to.valueOf();\n\n options.xaxis = {\n timezone: this.dashboard.getTimezone(),\n show: this.panel.xaxis.show,\n mode: 'time',\n min: min,\n max: max,\n label: 'Datetime',\n ticks: ticks,\n timeformat: graphTimeFormat(ticks, min, max),\n tickFormatter: graphTickFormatter,\n };\n }\n\n addXSeriesAxis(options: any) {\n const ticks = map(this.data, (series, index) => {\n return [index + 1, series.alias];\n });\n\n options.xaxis = {\n timezone: this.dashboard.getTimezone(),\n show: this.panel.xaxis.show,\n mode: null,\n min: 0,\n max: ticks.length + 1,\n label: 'Datetime',\n ticks: ticks,\n };\n }\n\n addXHistogramAxis(options: any, bucketSize: number) {\n let ticks: number | number[];\n let min: number | undefined;\n let max: number | undefined;\n\n const defaultTicks = this.panelWidth / 50;\n\n if (this.data.length && bucketSize) {\n const tickValues = [];\n\n for (const d of this.data) {\n for (const point of d.data) {\n tickValues[point[0]] = true;\n }\n }\n\n ticks = Object.keys(tickValues).map((v) => Number(v));\n min = _min(ticks)!;\n max = _max(ticks)!;\n\n // Adjust tick step\n let tickStep = bucketSize;\n let ticksNum = Math.floor((max - min) / tickStep);\n while (ticksNum > defaultTicks) {\n tickStep = tickStep * 2;\n ticksNum = Math.ceil((max - min) / tickStep);\n }\n\n // Expand ticks for pretty view\n min = Math.floor(min / tickStep) * tickStep;\n // 1.01 is 101% - ensure we have enough space for last bar\n max = Math.ceil((max * 1.01) / tickStep) * tickStep;\n\n ticks = [];\n for (let i = min; i <= max; i += tickStep) {\n ticks.push(i);\n }\n } else {\n // Set defaults if no data\n ticks = defaultTicks / 2;\n min = 0;\n max = 1;\n }\n\n options.xaxis = {\n timezone: this.dashboard.getTimezone(),\n show: this.panel.xaxis.show,\n mode: null,\n min: min,\n max: max,\n label: 'Histogram',\n ticks: ticks,\n };\n\n // Use 'short' format for histogram values\n this.configureAxisMode(options.xaxis, 'short', null);\n }\n\n addXTableAxis(options: any) {\n let ticks = map(this.data, (series, seriesIndex) => {\n return map(series.datapoints, (point, pointIndex) => {\n const tickIndex = seriesIndex * series.datapoints.length + pointIndex;\n return [tickIndex + 1, point[1]];\n });\n });\n // @ts-ignore, potential bug? is this flattenDeep?\n ticks = flatten(ticks, true);\n\n options.xaxis = {\n timezone: this.dashboard.getTimezone(),\n show: this.panel.xaxis.show,\n mode: null,\n min: 0,\n max: ticks.length + 1,\n label: 'Datetime',\n ticks: ticks,\n };\n }\n\n configureYAxisOptions(data: any, options: any) {\n const defaults = {\n position: 'left',\n show: this.panel.yaxes[0].show,\n index: 1,\n logBase: this.panel.yaxes[0].logBase || 1,\n min: this.parseNumber(this.panel.yaxes[0].min),\n max: this.parseNumber(this.panel.yaxes[0].max),\n tickDecimals: this.panel.yaxes[0].decimals,\n };\n\n options.yaxes.push(defaults);\n\n if (find(data, { yaxis: 2 })) {\n const secondY = clone(defaults);\n secondY.index = 2;\n secondY.show = this.panel.yaxes[1].show;\n secondY.logBase = this.panel.yaxes[1].logBase || 1;\n secondY.position = 'right';\n secondY.min = this.parseNumber(this.panel.yaxes[1].min);\n secondY.max = this.parseNumber(this.panel.yaxes[1].max);\n secondY.tickDecimals = this.panel.yaxes[1].decimals;\n options.yaxes.push(secondY);\n\n this.applyLogScale(options.yaxes[1], data);\n this.configureAxisMode(\n options.yaxes[1],\n this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format,\n this.panel.yaxes[1].decimals\n );\n }\n this.applyLogScale(options.yaxes[0], data);\n this.configureAxisMode(\n options.yaxes[0],\n this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format,\n this.panel.yaxes[0].decimals\n );\n }\n\n parseNumber(value: any) {\n if (value === null || typeof value === 'undefined') {\n return null;\n }\n\n return toNumber(value);\n }\n\n applyLogScale(axis: any, data: any) {\n if (axis.logBase === 1) {\n return;\n }\n\n const minSetToZero = axis.min === 0;\n\n if (axis.min < Number.MIN_VALUE) {\n axis.min = null;\n }\n if (axis.max < Number.MIN_VALUE) {\n axis.max = null;\n }\n\n let series, i;\n let max = axis.max,\n min = axis.min;\n\n for (i = 0; i < data.length; i++) {\n series = data[i];\n if (series.yaxis === axis.index) {\n if (!max || max < series.stats.max) {\n max = series.stats.max;\n }\n if (!min || min > series.stats.logmin) {\n min = series.stats.logmin;\n }\n }\n }\n\n axis.transform = (v: number) => {\n return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);\n };\n axis.inverseTransform = (v: any) => {\n return Math.pow(axis.logBase, v);\n };\n\n if (!max && !min) {\n max = axis.inverseTransform(+2);\n min = axis.inverseTransform(-2);\n } else if (!max) {\n max = min * axis.inverseTransform(+4);\n } else if (!min) {\n min = max * axis.inverseTransform(-4);\n }\n\n if (axis.min) {\n min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));\n } else {\n min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));\n }\n if (axis.max) {\n max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));\n } else {\n max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));\n }\n\n if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {\n return;\n }\n\n if (Number.isFinite(min) && Number.isFinite(max)) {\n if (minSetToZero) {\n axis.min = 0.1;\n min = 1;\n }\n\n axis.ticks = this.generateTicksForLogScaleYAxis(min, max, axis.logBase);\n if (minSetToZero) {\n axis.ticks.unshift(0.1);\n }\n if (axis.ticks[axis.ticks.length - 1] > axis.max) {\n axis.max = axis.ticks[axis.ticks.length - 1];\n }\n } else {\n axis.ticks = [1, 2];\n delete axis.min;\n delete axis.max;\n }\n }\n\n generateTicksForLogScaleYAxis(min: any, max: number, logBase: number) {\n let ticks = [];\n\n let nextTick;\n for (nextTick = min; nextTick <= max; nextTick *= logBase) {\n ticks.push(nextTick);\n }\n\n const maxNumTicks = Math.ceil(this.ctrl.height / 25);\n const numTicks = ticks.length;\n if (numTicks > maxNumTicks) {\n const factor = Math.ceil(numTicks / maxNumTicks) * logBase;\n ticks = [];\n\n for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {\n ticks.push(nextTick);\n }\n }\n\n return ticks;\n }\n\n configureAxisMode(axis: { tickFormatter: (val: any, axis: any) => string }, format: string, decimals?: DecimalCount) {\n axis.tickFormatter = (val, axis) => {\n const formatter = getValueFormat(format);\n\n if (!formatter) {\n throw new Error(`Unit '${format}' is not supported`);\n }\n\n return formattedValueToString(formatter(val, decimals));\n };\n }\n}\n\ncoreModule.directive('grafanaGraph', ['timeSrv', 'popoverSrv', 'contextSrv', graphDirective]);\n\nfunction graphDirective(timeSrv: TimeSrv, popoverSrv: any, contextSrv: ContextSrv) {\n return {\n restrict: 'A',\n template: '',\n link: (scope: any, elem: JQuery) => {\n return new GraphElement(scope, elem, timeSrv);\n },\n };\n}\n\nexport { GraphElement, graphDirective };\n","import { map, each, isUndefined } from 'lodash';\n\nimport { textUtil } from '@grafana/data';\nimport coreModule from 'app/angular/core_module';\n\ncoreModule.controller('SeriesOverridesCtrl', ['$scope', '$element', 'popoverSrv', SeriesOverridesCtrl]);\n\nexport function SeriesOverridesCtrl($scope: any, $element: JQuery, popoverSrv: any) {\n $scope.overrideMenu = [];\n $scope.currentOverrides = [];\n $scope.override = $scope.override || {};\n $scope.colorPickerModel = {};\n\n $scope.addOverrideOption = (name: string, propertyName: string, values: any) => {\n const option = {\n text: name,\n propertyName: propertyName,\n index: $scope.overrideMenu.length,\n values,\n submenu: map(values, (value) => {\n return { text: String(value), value: value };\n }),\n };\n\n $scope.overrideMenu.push(option);\n };\n\n $scope.setOverride = (item: { propertyName: string }, subItem: { value: any }) => {\n // handle color overrides\n if (item.propertyName === 'color') {\n $scope.openColorSelector($scope.override['color']);\n return;\n }\n\n $scope.override[item.propertyName] = subItem.value;\n\n // automatically disable lines for this series and the fill below to series\n // can be removed by the user if they still want lines\n if (item.propertyName === 'fillBelowTo') {\n $scope.override['lines'] = false;\n $scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false });\n }\n\n $scope.updateCurrentOverrides();\n $scope.ctrl.render();\n };\n\n $scope.colorSelected = (color: any) => {\n $scope.override['color'] = color;\n $scope.updateCurrentOverrides();\n $scope.ctrl.render();\n\n // update picker model so that the picker UI will also update\n $scope.colorPickerModel.series.color = color;\n };\n\n $scope.openColorSelector = (color: any) => {\n $scope.colorPickerModel = {\n autoClose: true,\n colorSelected: $scope.colorSelected,\n series: { color },\n };\n\n popoverSrv.show({\n element: $element.find('.dropdown')[0],\n position: 'top center',\n openOn: 'click',\n template: '',\n classNames: 'drop-popover drop-popover--transparent',\n model: $scope.colorPickerModel,\n onClose: () => {\n $scope.ctrl.render();\n },\n });\n };\n\n $scope.removeOverride = (option: { propertyName: string | number }) => {\n delete $scope.override[option.propertyName];\n $scope.updateCurrentOverrides();\n $scope.ctrl.refresh();\n };\n\n $scope.getSeriesNames = () => {\n return map($scope.ctrl.seriesList, (series) => {\n return textUtil.escapeHtml(series.alias);\n });\n };\n\n $scope.updateCurrentOverrides = () => {\n $scope.currentOverrides = [];\n each($scope.overrideMenu, (option) => {\n const value = $scope.override[option.propertyName];\n if (isUndefined(value)) {\n return;\n }\n $scope.currentOverrides.push({\n name: option.text,\n propertyName: option.propertyName,\n value: String(value),\n });\n });\n };\n\n $scope.addOverrideOption('Bars', 'bars', [true, false]);\n $scope.addOverrideOption('Lines', 'lines', [true, false]);\n $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);\n $scope.addOverrideOption('Fill gradient', 'fillGradient', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);\n $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);\n $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']);\n $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());\n $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);\n $scope.addOverrideOption('Dashes', 'dashes', [true, false]);\n $scope.addOverrideOption('Hidden Series', 'hiddenSeries', [true, false]);\n $scope.addOverrideOption(\n 'Dash Length',\n 'dashLength',\n [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]\n );\n $scope.addOverrideOption(\n 'Dash Space',\n 'spaceLength',\n [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]\n );\n $scope.addOverrideOption('Points', 'points', [true, false]);\n $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]);\n $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);\n $scope.addOverrideOption('Color', 'color', ['change']);\n $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);\n $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]);\n $scope.addOverrideOption('Transform', 'transform', ['constant', 'negative-Y']);\n $scope.addOverrideOption('Legend', 'legend', [true, false]);\n $scope.addOverrideOption('Hide in tooltip', 'hideTooltip', [true, false]);\n $scope.updateCurrentOverrides();\n}\n","import tinycolor from 'tinycolor2';\n\nimport coreModule from 'app/angular/core_module';\nimport config from 'app/core/config';\nexport class ThresholdFormCtrl {\n panelCtrl: any;\n panel: any;\n disabled = false;\n\n static $inject = ['$scope'];\n\n constructor(private $scope: any) {}\n\n $onInit() {\n this.panel = this.panelCtrl.panel;\n\n if (this.panel.alert && !config.unifiedAlertingEnabled) {\n this.disabled = true;\n }\n\n const unbindDestroy = this.$scope.$on('$destroy', () => {\n this.panelCtrl.editingThresholds = false;\n this.panelCtrl.render();\n unbindDestroy();\n });\n\n this.panelCtrl.editingThresholds = true;\n }\n\n addThreshold() {\n this.panel.thresholds.push({\n value: undefined,\n colorMode: 'critical',\n op: 'gt',\n fill: true,\n line: true,\n yaxis: 'left',\n });\n this.panelCtrl.render();\n }\n\n removeThreshold(index: number) {\n this.panel.thresholds.splice(index, 1);\n this.panelCtrl.render();\n }\n\n render() {\n this.panelCtrl.render();\n }\n\n onFillColorChange(index: number) {\n return (newColor: string) => {\n this.panel.thresholds[index].fillColor = newColor;\n this.render();\n };\n }\n\n onLineColorChange(index: number) {\n return (newColor: string) => {\n this.panel.thresholds[index].lineColor = newColor;\n this.render();\n };\n }\n\n onThresholdTypeChange(index: number) {\n // Because of the ng-model binding, threshold's color mode is already set here\n if (this.panel.thresholds[index].colorMode === 'custom') {\n this.panel.thresholds[index].fillColor = tinycolor(config.theme2.v1.palette.blue85).setAlpha(0.2).toRgbString();\n this.panel.thresholds[index].lineColor = tinycolor(config.theme2.v1.palette.blue77).setAlpha(0.6).toRgbString();\n }\n this.panelCtrl.render();\n }\n}\n\ncoreModule.directive('graphThresholdForm', () => {\n return {\n restrict: 'E',\n templateUrl: 'public/app/plugins/panel/graph/thresholds_form.html',\n controller: ThresholdFormCtrl,\n bindToController: true,\n controllerAs: 'ctrl',\n scope: {\n panelCtrl: '=',\n },\n };\n});\n","import coreModule from 'app/angular/core_module';\n\nimport { getColorModes } from './time_region_manager';\n\nexport class TimeRegionFormCtrl {\n panelCtrl: any;\n panel: any;\n disabled = false;\n colorModes: any;\n\n static $inject = ['$scope'];\n\n constructor(private $scope: any) {}\n\n $onInit() {\n this.panel = this.panelCtrl.panel;\n\n const unbindDestroy = this.$scope.$on('$destroy', () => {\n this.panelCtrl.editingTimeRegions = false;\n this.panelCtrl.render();\n unbindDestroy();\n });\n\n this.colorModes = getColorModes();\n this.panelCtrl.editingTimeRegions = true;\n }\n\n render() {\n this.panelCtrl.render();\n }\n\n addTimeRegion() {\n this.panel.timeRegions.push({\n op: 'time',\n fromDayOfWeek: undefined,\n from: undefined,\n toDayOfWeek: undefined,\n to: undefined,\n colorMode: 'background6',\n fill: true,\n line: false,\n // Default colors for new\n fillColor: 'rgba(234, 112, 112, 0.12)',\n lineColor: 'rgba(237, 46, 24, 0.60)',\n });\n this.panelCtrl.render();\n }\n\n removeTimeRegion(index: number) {\n this.panel.timeRegions.splice(index, 1);\n this.panelCtrl.render();\n }\n\n onFillColorChange(index: number) {\n return (newColor: string) => {\n this.panel.timeRegions[index].fillColor = newColor;\n this.render();\n };\n }\n\n onLineColorChange(index: number) {\n return (newColor: string) => {\n this.panel.timeRegions[index].lineColor = newColor;\n this.render();\n };\n }\n}\n\ncoreModule.directive('graphTimeRegionForm', () => {\n return {\n restrict: 'E',\n templateUrl: 'public/app/plugins/panel/graph/time_regions_form.html',\n controller: TimeRegionFormCtrl,\n bindToController: true,\n controllerAs: 'ctrl',\n scope: {\n panelCtrl: '=',\n },\n };\n});\n","import $ from 'jquery';\nimport { isString, escape } from 'lodash';\n\nimport coreModule from 'app/angular/core_module';\nimport alertDef from 'app/features/alerting/state/alertDef';\nimport { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';\n\ncoreModule.directive('annotationTooltip', ['$sanitize', 'dashboardSrv', '$compile', annotationTooltipDirective]);\n\nexport function annotationTooltipDirective($sanitize: any, dashboardSrv: DashboardSrv, $compile: any) {\n function sanitizeString(str: string) {\n try {\n return $sanitize(str);\n } catch (err) {\n console.log('Could not sanitize annotation string, html escaping instead');\n return escape(str);\n }\n }\n\n return {\n restrict: 'E',\n scope: {\n event: '=',\n onEdit: '&',\n },\n link: (scope: any, element: JQuery) => {\n const event = scope.event;\n let title = event.title;\n let text = event.text;\n const dashboard = dashboardSrv.getCurrent();\n\n let tooltip = '';\n let titleStateClass = '';\n\n if (event.alertId !== undefined && event.newState) {\n const stateModel = alertDef.getStateDisplayModel(event.newState);\n titleStateClass = stateModel.stateClass;\n title = `
${stateModel.text}`;\n text = alertDef.getAlertAnnotationInfo(event);\n if (event.text) {\n text = text + '
' + event.text;\n }\n } else if (title) {\n text = title + '
' + (isString(text) ? text : '');\n title = '';\n }\n\n let header = ``;\n tooltip += header;\n tooltip += '
';\n\n if (text) {\n tooltip += '
' + sanitizeString(text.replace(/\\n/g, '
')) + '
';\n }\n\n const tags = event.tags;\n\n if (tags && tags.length) {\n scope.tags = tags;\n tooltip +=\n '
{{tag}}';\n }\n\n tooltip += '
';\n tooltip += '
';\n\n const $tooltip = $(tooltip);\n $tooltip.appendTo(element);\n\n $compile(element.contents())(scope);\n },\n };\n}\n","import { cloneDeep, isNumber } from 'lodash';\n\nimport { AnnotationEvent, dateTime } from '@grafana/data';\nimport { coreModule } from 'app/angular/core_module';\nimport { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl';\n\nimport { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../../features/annotations/api';\nimport { getDashboardQueryRunner } from '../../../features/query/state/DashboardQueryRunner/DashboardQueryRunner';\n\nexport class EventEditorCtrl {\n // @ts-ignore initialized through Angular not constructor\n panelCtrl: MetricsPanelCtrl;\n // @ts-ignore initialized through Angular not constructor\n event: AnnotationEvent;\n timeRange?: { from: number; to: number };\n form: any;\n close: any;\n timeFormated?: string;\n\n constructor() {}\n\n $onInit() {\n this.event.panelId = this.panelCtrl.panel.id; // set correct id if in panel edit\n this.event.dashboardUID = this.panelCtrl.dashboard.uid;\n\n // Annotations query returns time as Unix timestamp in milliseconds\n this.event.time = tryEpochToMoment(this.event.time);\n if (this.event.isRegion) {\n this.event.timeEnd = tryEpochToMoment(this.event.timeEnd);\n }\n\n this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time!);\n }\n\n canDelete(): boolean {\n if (this.event.source?.type === 'dashboard') {\n return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.dashboard.canDelete;\n }\n return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.organization.canDelete;\n }\n\n async save(): Promise {\n if (!this.form.$valid) {\n return;\n }\n\n const saveModel = cloneDeep(this.event);\n saveModel.time = saveModel.time!.valueOf();\n saveModel.timeEnd = 0;\n\n if (saveModel.isRegion) {\n saveModel.timeEnd = this.event.timeEnd!.valueOf();\n\n if (saveModel.timeEnd < saveModel.time) {\n console.log('invalid time');\n return;\n }\n }\n\n let crudFunction = saveAnnotation;\n if (saveModel.id) {\n crudFunction = updateAnnotation;\n }\n\n try {\n await crudFunction(saveModel);\n } catch (err) {\n console.log(err);\n } finally {\n this.close();\n getDashboardQueryRunner().run({ dashboard: this.panelCtrl.dashboard, range: this.panelCtrl.range });\n }\n }\n\n async delete(): Promise {\n try {\n await deleteAnnotation(this.event);\n } catch (err) {\n console.log(err);\n } finally {\n this.close();\n getDashboardQueryRunner().run({ dashboard: this.panelCtrl.dashboard, range: this.panelCtrl.range });\n }\n }\n}\n\nfunction tryEpochToMoment(timestamp: any) {\n if (timestamp && isNumber(timestamp)) {\n const epoch = Number(timestamp);\n return dateTime(epoch);\n } else {\n return timestamp;\n }\n}\n\nexport function eventEditor() {\n return {\n restrict: 'E',\n controller: EventEditorCtrl,\n bindToController: true,\n controllerAs: 'ctrl',\n templateUrl: 'public/app/features/annotations/partials/event_editor.html',\n scope: {\n panelCtrl: '=',\n event: '=',\n close: '&',\n },\n };\n}\n\ncoreModule.directive('eventEditor', eventEditor);\n","import { FlotDataPoint } from '@grafana/data';\nimport { MenuItemProps } from '@grafana/ui';\n\nexport class GraphContextMenuCtrl {\n private source?: FlotDataPoint | null;\n private scope?: any;\n menuItemsSupplier?: () => MenuItemProps[];\n scrollContextElement: HTMLElement | null = null;\n position: {\n x: number;\n y: number;\n } = { x: 0, y: 0 };\n\n isVisible: boolean;\n\n constructor($scope: any) {\n this.isVisible = false;\n this.scope = $scope;\n }\n\n onClose = () => {\n if (this.scrollContextElement) {\n this.scrollContextElement.removeEventListener('scroll', this.onClose);\n }\n\n this.scope.$apply(() => {\n this.isVisible = false;\n });\n };\n\n toggleMenu = (event?: { pageX: number; pageY: number }) => {\n this.isVisible = !this.isVisible;\n if (this.isVisible && this.scrollContextElement) {\n this.scrollContextElement.addEventListener('scroll', this.onClose);\n }\n\n if (this.source) {\n this.position = {\n x: this.source.pageX,\n y: this.source.pageY,\n };\n } else {\n this.position = {\n x: event ? event.pageX : 0,\n y: event ? event.pageY : 0,\n };\n }\n };\n\n // Sets element which is considered as a scroll context of given context menu.\n // Having access to this element allows scroll event attachement for menu to be closed when user scrolls\n setScrollContextElement = (el: HTMLElement | null) => {\n this.scrollContextElement = el;\n };\n\n setSource = (source: FlotDataPoint | null) => {\n this.source = source;\n };\n\n getSource = () => {\n return this.source;\n };\n\n setMenuItemsSupplier = (menuItemsSupplier: () => MenuItemProps[]) => {\n this.menuItemsSupplier = menuItemsSupplier;\n };\n}\n","import { PanelModel, FieldConfigSource, DataLink } from '@grafana/data';\n\n/**\n * Called when upgrading from a previously saved versoin\n */\nexport const graphPanelMigrationHandler = (panel: PanelModel): Partial => {\n const fieldConfig: FieldConfigSource = panel.fieldConfig ?? {\n defaults: {},\n overrides: [],\n };\n\n const options = panel.options || {};\n\n // Move <7.1 dataLinks to the field section\n if (options.dataLinks) {\n fieldConfig.defaults.links = options.dataLinks as DataLink[];\n delete options.dataLinks;\n }\n\n // Mutate the original panel state (only necessary because it is angular)\n panel.options = options;\n panel.fieldConfig = fieldConfig;\n return options;\n};\n","import { selectors } from '@grafana/e2e-selectors';\n\nimport { GraphCtrl } from './module';\n\nexport class AxesEditorCtrl {\n panel: any;\n panelCtrl: GraphCtrl;\n logScales: any;\n xAxisModes: any;\n xAxisStatOptions: any;\n xNameSegment: any;\n selectors: typeof selectors.components.Panels.Visualization.Graph.VisualizationTab;\n\n static $inject = ['$scope'];\n\n constructor(private $scope: any) {\n this.panelCtrl = $scope.ctrl as GraphCtrl;\n this.panel = this.panelCtrl.panel;\n this.$scope.ctrl = this;\n\n this.logScales = {\n linear: 1,\n 'log (base 2)': 2,\n 'log (base 10)': 10,\n 'log (base 32)': 32,\n 'log (base 1024)': 1024,\n };\n\n this.xAxisModes = {\n Time: 'time',\n Series: 'series',\n Histogram: 'histogram',\n // 'Data field': 'field',\n };\n\n this.xAxisStatOptions = [\n { text: 'Avg', value: 'avg' },\n { text: 'Min', value: 'min' },\n { text: 'Max', value: 'max' },\n { text: 'Total', value: 'total' },\n { text: 'Count', value: 'count' },\n { text: 'Current', value: 'current' },\n ];\n\n if (this.panel.xaxis.mode === 'custom') {\n if (!this.panel.xaxis.name) {\n this.panel.xaxis.name = 'specify field';\n }\n }\n this.selectors = selectors.components.Panels.Visualization.Graph.VisualizationTab;\n }\n\n setUnitFormat(axis: { format: any }) {\n return (unit: string) => {\n axis.format = unit;\n // if already set via field config we clear that\n if (this.panel.fieldConfig.defaults.unit) {\n this.panel.fieldConfig.defaults.unit = undefined;\n this.panelCtrl.refresh();\n } else {\n this.panelCtrl.render();\n }\n };\n }\n\n render() {\n this.panelCtrl.render();\n }\n\n xAxisModeChanged() {\n this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode();\n this.panelCtrl.onDataFramesReceived(this.panelCtrl.dataList);\n }\n\n xAxisValueChanged() {\n this.panelCtrl.onDataFramesReceived(this.panelCtrl.dataList);\n }\n}\n\nexport function axesEditorComponent() {\n 'use strict';\n return {\n restrict: 'E',\n scope: true,\n templateUrl: 'public/app/plugins/panel/graph/axes_editor.html',\n controller: AxesEditorCtrl,\n };\n}\n","import { find } from 'lodash';\n\nimport { DataFrame, dateTime, Field, FieldType, getFieldDisplayName, getTimeField, TimeRange } from '@grafana/data';\nimport { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold';\nimport { colors } from '@grafana/ui';\nimport config from 'app/core/config';\nimport TimeSeries from 'app/core/time_series2';\n\ntype Options = {\n dataList: DataFrame[];\n range?: TimeRange;\n};\n\nexport class DataProcessor {\n constructor(private panel: any) {}\n\n getSeriesList(options: Options): TimeSeries[] {\n const list: TimeSeries[] = [];\n const { dataList, range } = options;\n\n if (!dataList || !dataList.length) {\n return list;\n }\n\n for (let i = 0; i < dataList.length; i++) {\n let series = dataList[i];\n let { timeField } = getTimeField(series);\n\n if (!timeField) {\n continue;\n }\n\n series = applyNullInsertThreshold({ frame: series, refFieldName: timeField.name });\n timeField = getTimeField(series).timeField!; // use updated length\n\n for (let j = 0; j < series.fields.length; j++) {\n const field = series.fields[j];\n\n if (field.type !== FieldType.number) {\n continue;\n }\n const name = getFieldDisplayName(field, series, dataList);\n const datapoints = [];\n\n for (let r = 0; r < series.length; r++) {\n datapoints.push([field.values[r], dateTime(timeField.values[r]).valueOf()]);\n }\n\n list.push(this.toTimeSeries(field, name, i, j, datapoints, list.length, range));\n }\n }\n\n // Merge all the rows if we want to show a histogram\n if (this.panel.xaxis.mode === 'histogram' && !this.panel.stack && list.length > 1) {\n const first = list[0];\n first.alias = first.aliasEscaped = 'Count';\n\n for (let i = 1; i < list.length; i++) {\n first.datapoints = first.datapoints.concat(list[i].datapoints);\n }\n\n return [first];\n }\n\n return list;\n }\n\n private toTimeSeries(\n field: Field,\n alias: string,\n dataFrameIndex: number,\n fieldIndex: number,\n datapoints: any[][],\n index: number,\n range?: TimeRange\n ) {\n const colorIndex = index % colors.length;\n const color = this.panel.aliasColors[alias] || colors[colorIndex];\n\n const series = new TimeSeries({\n datapoints: datapoints || [],\n alias: alias,\n color: config.theme2.visualization.getColorByName(color),\n unit: field.config ? field.config.unit : undefined,\n dataFrameIndex,\n fieldIndex,\n });\n\n if (datapoints && datapoints.length > 0 && range) {\n const last = datapoints[datapoints.length - 1][1];\n const from = range.from;\n\n if (last - from.valueOf() < -10000) {\n // If the data is in reverse order\n const first = datapoints[0][1];\n if (first - from.valueOf() < -10000) {\n series.isOutsideRange = true;\n }\n }\n }\n return series;\n }\n\n setPanelDefaultsForNewXAxisMode() {\n switch (this.panel.xaxis.mode) {\n case 'time': {\n this.panel.bars = false;\n this.panel.lines = true;\n this.panel.points = false;\n this.panel.legend.show = true;\n this.panel.tooltip.shared = true;\n this.panel.xaxis.values = [];\n break;\n }\n case 'series': {\n this.panel.bars = true;\n this.panel.lines = false;\n this.panel.points = false;\n this.panel.stack = false;\n this.panel.legend.show = false;\n this.panel.tooltip.shared = false;\n this.panel.xaxis.values = ['total'];\n break;\n }\n case 'histogram': {\n this.panel.bars = true;\n this.panel.lines = false;\n this.panel.points = false;\n this.panel.stack = false;\n this.panel.legend.show = false;\n this.panel.tooltip.shared = false;\n break;\n }\n }\n }\n\n validateXAxisSeriesValue() {\n switch (this.panel.xaxis.mode) {\n case 'series': {\n if (this.panel.xaxis.values.length === 0) {\n this.panel.xaxis.values = ['total'];\n return;\n }\n\n const validOptions = this.getXAxisValueOptions({});\n const found: any = find(validOptions, { value: this.panel.xaxis.values[0] });\n if (!found) {\n this.panel.xaxis.values = ['total'];\n }\n return;\n }\n }\n }\n\n getXAxisValueOptions(options: any) {\n switch (this.panel.xaxis.mode) {\n case 'series': {\n return [\n { text: 'Avg', value: 'avg' },\n { text: 'Min', value: 'min' },\n { text: 'Max', value: 'max' },\n { text: 'Total', value: 'total' },\n { text: 'Count', value: 'count' },\n ];\n }\n }\n\n return [];\n }\n\n pluckDeep(obj: any, property: string) {\n const propertyParts = property.split('.');\n let value = obj;\n for (let i = 0; i < propertyParts.length; ++i) {\n if (value[propertyParts[i]]) {\n value = value[propertyParts[i]];\n } else {\n return undefined;\n }\n }\n return value;\n }\n}\n","const template = `\n\n`;\n\nexport default template;\n","import './graph';\nimport './series_overrides_ctrl';\nimport './thresholds_form';\nimport './time_regions_form';\nimport './annotation_tooltip';\nimport './event_editor';\n\nimport { auto } from 'angular';\nimport { defaults, find, without } from 'lodash';\n\nimport { DataFrame, FieldConfigProperty, PanelEvents, PanelPlugin } from '@grafana/data';\nimport { locationService } from '@grafana/runtime';\nimport { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl';\nimport config from 'app/core/config';\nimport TimeSeries from 'app/core/time_series2';\nimport { ThresholdMapper } from 'app/features/alerting/state/ThresholdMapper';\nimport { changePanelPlugin } from 'app/features/panel/state/actions';\nimport { dispatch } from 'app/store/store';\n\nimport { appEvents } from '../../../core/core';\nimport { loadSnapshotData } from '../../../features/dashboard/utils/loadSnapshotData';\nimport { annotationsFromDataFrames } from '../../../features/query/state/DashboardQueryRunner/utils';\nimport { ZoomOutEvent } from '../../../types/events';\n\nimport { GraphContextMenuCtrl } from './GraphContextMenuCtrl';\nimport { graphPanelMigrationHandler } from './GraphMigrations';\nimport { axesEditorComponent } from './axes_editor';\nimport { DataProcessor } from './data_processor';\nimport template from './template';\nimport { DataWarning, GraphFieldConfig, GraphPanelOptions } from './types';\nimport { getDataTimeRange } from './utils';\n\nexport class GraphCtrl extends MetricsPanelCtrl {\n static template = template;\n\n renderError = false;\n hiddenSeries: any = {};\n hiddenSeriesTainted = false;\n seriesList: TimeSeries[] = [];\n dataList: DataFrame[] = [];\n annotations: any = [];\n alertState: any;\n\n dataWarning?: DataWarning;\n colors: any = [];\n subTabIndex = 0;\n processor: DataProcessor;\n contextMenuCtrl: GraphContextMenuCtrl;\n\n panelDefaults: any = {\n // datasource name, null = default datasource\n datasource: null,\n // sets client side (flot) or native graphite png renderer (png)\n renderer: 'flot',\n yaxes: [\n {\n label: null,\n show: true,\n logBase: 1,\n min: null,\n max: null,\n format: 'short',\n },\n {\n label: null,\n show: true,\n logBase: 1,\n min: null,\n max: null,\n format: 'short',\n },\n ],\n xaxis: {\n show: true,\n mode: 'time',\n name: null,\n values: [],\n buckets: null,\n },\n yaxis: {\n align: false,\n alignLevel: null,\n },\n // show/hide lines\n lines: true,\n // fill factor\n fill: 1,\n // fill gradient\n fillGradient: 0,\n // line width in pixels\n linewidth: 1,\n // show/hide dashed line\n dashes: false,\n // show/hide line\n hiddenSeries: false,\n // length of a dash\n dashLength: 10,\n // length of space between two dashes\n spaceLength: 10,\n // show hide points\n points: false,\n // point radius in pixels\n pointradius: 2,\n // show hide bars\n bars: false,\n // enable/disable stacking\n stack: false,\n // stack percentage mode\n percentage: false,\n // legend options\n legend: {\n show: true, // disable/enable legend\n values: false, // disable/enable legend values\n min: false,\n max: false,\n current: false,\n total: false,\n avg: false,\n },\n // how null points should be handled\n nullPointMode: 'null',\n // staircase line mode\n steppedLine: false,\n // tooltip options\n tooltip: {\n value_type: 'individual',\n shared: true,\n sort: 0,\n },\n // time overrides\n timeFrom: null,\n timeShift: null,\n // metric queries\n targets: [{}],\n // series color overrides\n aliasColors: {},\n // other style overrides\n seriesOverrides: [],\n thresholds: [],\n timeRegions: [],\n options: {\n // show/hide alert threshold lines and fill\n alertThreshold: true,\n },\n };\n\n static $inject = ['$scope', '$injector'];\n\n constructor($scope: any, $injector: auto.IInjectorService) {\n super($scope, $injector);\n\n defaults(this.panel, this.panelDefaults);\n defaults(this.panel.tooltip, this.panelDefaults.tooltip);\n defaults(this.panel.legend, this.panelDefaults.legend);\n defaults(this.panel.xaxis, this.panelDefaults.xaxis);\n defaults(this.panel.options, this.panelDefaults.options);\n\n this.useDataFrames = true;\n this.processor = new DataProcessor(this.panel);\n this.contextMenuCtrl = new GraphContextMenuCtrl($scope);\n\n this.events.on(PanelEvents.render, this.onRender.bind(this));\n this.events.on(PanelEvents.dataFramesReceived, this.onDataFramesReceived.bind(this));\n this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this));\n this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));\n this.events.on(PanelEvents.initPanelActions, this.onInitPanelActions.bind(this));\n\n // set axes format from field config\n const fieldConfigUnit = this.panel.fieldConfig.defaults.unit;\n if (fieldConfigUnit) {\n this.panel.yaxes[0].format = fieldConfigUnit;\n }\n }\n\n onInitEditMode() {\n this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html');\n this.addEditorTab('Series overrides', 'public/app/plugins/panel/graph/tab_series_overrides.html');\n this.addEditorTab('Axes', axesEditorComponent);\n this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html');\n this.addEditorTab('Thresholds', 'public/app/plugins/panel/graph/tab_thresholds.html');\n this.addEditorTab('Time regions', 'public/app/plugins/panel/graph/tab_time_regions.html');\n this.subTabIndex = 0;\n this.hiddenSeriesTainted = false;\n }\n\n onInitPanelActions(actions: any[]) {\n actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()', shortcut: 'p l' });\n }\n\n zoomOut(evt: any) {\n appEvents.publish(new ZoomOutEvent({ scale: 2 }));\n }\n\n onDataSnapshotLoad(snapshotData: any) {\n const { series, annotations } = loadSnapshotData(this.panel, this.dashboard);\n this.panelData!.annotations = annotations;\n this.onDataFramesReceived(series);\n }\n\n onDataFramesReceived(data: DataFrame[]) {\n this.dataList = data;\n this.seriesList = this.processor.getSeriesList({\n dataList: this.dataList,\n range: this.range,\n });\n\n this.dataWarning = this.getDataWarning();\n\n this.alertState = undefined;\n (this.seriesList as any).alertState = undefined;\n if (this.panelData!.alertState) {\n this.alertState = this.panelData!.alertState;\n (this.seriesList as any).alertState = this.alertState.state;\n }\n\n this.annotations = [];\n if (this.panelData!.annotations?.length) {\n this.annotations = annotationsFromDataFrames(this.panelData!.annotations);\n }\n\n this.loading = false;\n this.render(this.seriesList);\n }\n\n getDataWarning(): DataWarning | undefined {\n const datapointsCount = this.seriesList.reduce((prev, series) => {\n return prev + series.datapoints.length;\n }, 0);\n\n if (datapointsCount === 0) {\n if (this.dataList) {\n for (const frame of this.dataList) {\n if (frame.length && frame.fields?.length) {\n return {\n title: 'Unable to graph data',\n tip: 'Data exists, but is not timeseries',\n actionText: 'Switch to table view',\n action: () => {\n dispatch(changePanelPlugin({ panel: this.panel, pluginId: 'table' }));\n },\n };\n }\n }\n }\n\n return {\n title: 'No data',\n tip: 'No data returned from query',\n };\n }\n\n // If any data is in range, do not return an error\n for (const series of this.seriesList) {\n if (!series.isOutsideRange) {\n return undefined;\n }\n }\n\n // All data is outside the time range\n const dataWarning: DataWarning = {\n title: 'Data outside time range',\n tip: 'Can be caused by timezone mismatch or missing time filter in query',\n };\n\n const range = getDataTimeRange(this.dataList);\n\n if (range) {\n dataWarning.actionText = 'Zoom to data';\n dataWarning.action = () => {\n locationService.partial({\n from: range.from,\n to: range.to,\n });\n };\n }\n\n return dataWarning;\n }\n\n onRender() {\n if (!this.seriesList) {\n return;\n }\n\n ThresholdMapper.alertToGraphThresholds(this.panel);\n\n for (const series of this.seriesList) {\n series.applySeriesOverrides(this.panel.seriesOverrides);\n\n // Always use the configured field unit\n if (series.unit) {\n this.panel.yaxes[series.yaxis - 1].format = series.unit;\n }\n if (this.hiddenSeriesTainted === false && series.hiddenSeries === true) {\n this.hiddenSeries[series.alias] = true;\n }\n }\n }\n\n onColorChange = (series: any, color: string) => {\n series.setColor(config.theme2.visualization.getColorByName(color));\n this.panel.aliasColors[series.alias] = color;\n this.render();\n };\n\n onToggleSeries = (hiddenSeries: any) => {\n this.hiddenSeriesTainted = true;\n this.hiddenSeries = hiddenSeries;\n this.render();\n };\n\n onToggleSort = (sortBy: any, sortDesc: any) => {\n this.panel.legend.sort = sortBy;\n this.panel.legend.sortDesc = sortDesc;\n this.render();\n };\n\n onToggleAxis = (info: { alias: any; yaxis: any }) => {\n let override: any = find(this.panel.seriesOverrides, { alias: info.alias });\n if (!override) {\n override = { alias: info.alias };\n this.panel.seriesOverrides.push(override);\n }\n override.yaxis = info.yaxis;\n this.render();\n };\n\n addSeriesOverride(override: any) {\n this.panel.seriesOverrides.push(override || {});\n }\n\n removeSeriesOverride(override: any) {\n this.panel.seriesOverrides = without(this.panel.seriesOverrides, override);\n this.render();\n }\n\n toggleLegend() {\n this.panel.legend.show = !this.panel.legend.show;\n this.render();\n }\n\n legendValuesOptionChanged() {\n const legend = this.panel.legend;\n legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total;\n this.render();\n }\n\n onContextMenuClose = () => {\n this.contextMenuCtrl.toggleMenu();\n };\n\n getTimeZone = () => this.dashboard.getTimezone();\n\n getDataFrameByRefId = (refId: string) => {\n return this.dataList.filter((dataFrame) => dataFrame.refId === refId)[0];\n };\n\n migrateToReact() {\n this.onPluginTypeChange(config.panels['timeseries']);\n }\n}\n\n// Use new react style configuration\nexport const plugin = new PanelPlugin(null)\n .useFieldConfig({\n disableStandardOptions: [\n FieldConfigProperty.NoValue,\n FieldConfigProperty.Thresholds,\n FieldConfigProperty.Max,\n FieldConfigProperty.Min,\n FieldConfigProperty.Decimals,\n FieldConfigProperty.Color,\n FieldConfigProperty.Mappings,\n ],\n })\n .setDataSupport({ annotations: true, alertStates: true })\n .setMigrationHandler(graphPanelMigrationHandler);\n\n// Use the angular ctrt rather than a react one\nplugin.angularPanelCtrl = GraphCtrl;\n"],"names":["DEFAULT_PORTS","AngularLocationWrapper","fn","replacement","self","newHash","pathname","location","parsedPath","url","search","paramValue","newQuery","key","updatedUrl","state","newUrl","MetricsPanelCtrl","$scope","$injector","data","timeInfo","legacy","v","queryRunner","err","datasource","newTimeData","panel","frame","result","PanelCtrl","plugin","event","payload","title","directiveFn","index","icon","editorTab","menu","hiddenReducerTypes","ThresholdMapper","i","condition","evaluator","thresholds","visible","value","value1","value2","t","loadSnapshotData","dashboard","worker","options","annotationEvents","annotations","createAnnotationToolip","element","plot","injector","content","$compile","$rootScope","eventManager","tmpScope","drop","markerElementToAttachTo","createEditPopover","scope","DrawableEvent","object","drawFunc","clearFunc","moveFunc","left","top","width","height","position","VisualEvent","drawableEvent","EventMarkers","types","events","parts","regions","ve","vre","a","b","ao","bo","insidePlot","overlapPlot","o","xaxis","val","that","container","color","markerSize","markerShow","lineStyle","lineWidth","markerTooltip","eventTypeId","topOffset","line","marker","mouseenter","mouseleave","obj","regionWidth","timeFrom","timeTo","right","xmin","xmax","regionStart","regionEnd","regionOffset","region","x","xc","point0","point1","coord0","coord1","coordMin","coordMax","init","eventMarkers","defaultOptions","LEGEND_STATS","LegendItem","props","e","yaxis","info","series","asTable","legendValueItems","valueName","valueFormatted","LegendValue","values","hidden","seriesOptionClasses","valueItems","seriesLabel","LegendSeriesLabel","label","onColorChange","onToggleAxis","onLabelClick","LegendSeriesIcon","selectors","ref","showColorPicker","hideColorPicker","SeriesIcon","onValueClick","GraphLegend","hiddenSeries","seriesList","sortBy","sort","optionalClass","rightSide","sideWidth","sortDesc","hideEmpty","hideZero","min","max","avg","current","total","renderCallback","seriesValuesProps","seriesHideProps","sortProps","legendClass","ieWidth","legendStyle","legendProps","LegendTable","LegendSeriesList","stat","statName","LegendTableHeaderItem","Icon","Legend","CustomScrollbar","alignYLevel","yAxes","level","checkCorrectAxis","yLeft","yRight","moveLevelToZero","expandStuckValues","zero","oneSide","checkOneSide","checkOppositeSides","rate","getRate","checkTwoCross","restoreLevelFromZero","axis","checkCorrectAxes","axes","rateLeft","rateRight","absLeftMin","absLeftMax","absRightMin","absRightMax","upLeft","downLeft","upRight","downRight","EventManager","panelCtrl","range","elem","flotOptions","item","getRegions","addRegionMarking","eventSectionHeight","eventSectionMargin","markings","defaultColor","fillColor","addAlphaToRGB","colorString","alpha","tinycolor","GraphTooltip","getSeriesFn","hoverEvent","$tooltip","posX","last","ps","initial","len","j","lower","upper","middle","absoluteTime","innerHtml","pos","xMode","hoverIndex","hoverDistance","pointTime","results","lastValue","minDistance","minTime","plotData","allSeriesMode","group","hoverInfo","seriesHtml","tooltipFormat","pointOffset","scrollTop","seriesHoverInfo","highlightClass","getSeriesValues","dataList","datapoints","convertValuesToHistogram","bucketSize","minBound","getBucketBound","maxBound","bin","convertToHistogramData","histogram","ThresholdManager","handleIndex","model","valueStr","stateClass","evt","handleElem","lastY","posTop","dragging","diff","stopped","graphValue","defaultHandleTopPos","handleTopPos","valueCanvasPos","thresholdMargin","gtLimit","ltLimit","threshold","other","limit","lineColor","config","colorModes","getColorModes","getColor","timeRegion","theme","colorMode","TimeRegionManager","tRange","tr","timeRegionColor","r","getDataTimeRange","frames","found","reducers","field","calcs","isLegacyGraphHoverEvent","graphTickFormatter","epoch","graphTimeFormat","ticks","secPerTick","oneDay","oneYear","LegendWithThemeProvider","GraphElement","timeSrv","flotPosition","linksSupplier","items","dataLinks","link","renderData","graphHeight","alignAsTable","legendOptions","valueOptions","legendReactElem","handler","ranges","scrollContextElement","contextMenuSourceItem","yAxisConfig","dataFrame","dataIndex","links","hasLinksValue","fieldConfig","fieldDisplay","DataFrameView","datapoint","ts","timeField","correctIndex","dataWarning","msg","gridMargin","panelOptions","align","histMin","s","histMax","incrementRenderCounter","gridColor","stack","sortOrder","haveSortBy","haveSortOrder","shouldSortBy","amount","fill","defaultTicks","tickValues","d","point","tickStep","ticksNum","seriesIndex","pointIndex","defaults","secondY","minSetToZero","logBase","nextTick","maxNumTicks","numTicks","factor","format","decimals","formatter","graphDirective","popoverSrv","contextSrv","SeriesOverridesCtrl","$element","name","propertyName","option","subItem","ThresholdFormCtrl","unbindDestroy","newColor","TimeRegionFormCtrl","annotationTooltipDirective","$sanitize","dashboardSrv","sanitizeString","str","text","tooltip","titleStateClass","stateModel","alertDef","header","tags","EventEditorCtrl","tryEpochToMoment","saveModel","crudFunction","timestamp","eventEditor","GraphContextMenuCtrl","el","source","menuItemsSupplier","graphPanelMigrationHandler","AxesEditorCtrl","unit","axesEditorComponent","DataProcessor","list","first","alias","dataFrameIndex","fieldIndex","colorIndex","colors","from","validOptions","property","propertyParts","GraphCtrl","override","refId","fieldConfigUnit","actions","snapshotData","prev","legend","PanelPlugin"],"sourceRoot":""}