diff --git a/packages/grafana-ui/src/components/VizTooltip/types.ts b/packages/grafana-ui/src/components/VizTooltip/types.ts index 4d33d3c2637..a33868ce7b5 100644 --- a/packages/grafana-ui/src/components/VizTooltip/types.ts +++ b/packages/grafana-ui/src/components/VizTooltip/types.ts @@ -19,11 +19,14 @@ export enum ColorPlacement { export interface LabelValue { label: string; - value: string | number | null; + value: string; color?: string; colorIndicator?: ColorIndicator; colorPlacement?: ColorPlacement; isActive?: boolean; + + // internal/tmp for sorting + numeric?: number; } export const DEFAULT_COLOR_INDICATOR = ColorIndicator.series; diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.test.ts b/packages/grafana-ui/src/components/VizTooltip/utils.test.ts index 92c59cdba5c..5c4189012bb 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.test.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.test.ts @@ -1,4 +1,7 @@ -import { calculateTooltipPosition } from './utils'; +import { DataFrame, FieldType } from '@grafana/data'; +import { SortOrder, TooltipDisplayMode } from '@grafana/schema'; + +import { calculateTooltipPosition, getContentItems } from './utils'; describe('utils', () => { describe('calculateTooltipPosition', () => { @@ -162,4 +165,171 @@ describe('utils', () => { }); }); }); + + describe('it tests getContentItems with numeric values', () => { + const timeValues = [1707833954056, 1707838274056, 1707842594056]; + const seriesAValues = [1, 20, 70]; + const seriesBValues = [-100, -26, null]; + + const frame = { + name: 'a', + length: timeValues.length, + fields: [ + { + name: 'time', + type: FieldType.time, + values: timeValues[0], + config: {}, + display: (value: string) => ({ + text: value, + color: undefined, + numeric: NaN, + }), + }, + { + name: 'A-series', + type: FieldType.number, + values: seriesAValues, + config: {}, + display: (value: string) => ({ + text: value, + color: undefined, + numeric: Number(value), + }), + }, + { + name: 'B-series', + type: FieldType.number, + values: seriesBValues, + config: {}, + display: (value: string) => ({ + text: value, + color: undefined, + numeric: Number(value), + }), + }, + ], + } as unknown as DataFrame; + + const fields = frame.fields; + const xField = frame.fields[0]; + const dataIdxs = [1, 1, 1]; + + it('displays one series in single mode', () => { + const rows = getContentItems(fields, xField, dataIdxs, 2, TooltipDisplayMode.Single, SortOrder.None); + expect(rows.length).toBe(1); + expect(rows[0].value).toBe('-26'); + }); + + it('displays the right content in multi mode', () => { + const rows = getContentItems(fields, xField, dataIdxs, null, TooltipDisplayMode.Multi, SortOrder.None); + expect(rows.length).toBe(2); + expect(rows[0].value).toBe('20'); + expect(rows[1].value).toBe('-26'); + }); + + it('displays the values sorted ASC', () => { + const rows = getContentItems(fields, xField, dataIdxs, null, TooltipDisplayMode.Multi, SortOrder.Ascending); + expect(rows.length).toBe(2); + expect(rows[0].value).toBe('-26'); + expect(rows[1].value).toBe('20'); + }); + + it('displays the values sorted DESC', () => { + const rows = getContentItems(fields, xField, dataIdxs, null, TooltipDisplayMode.Multi, SortOrder.Descending); + expect(rows.length).toBe(2); + expect(rows[0].value).toBe('20'); + expect(rows[1].value).toBe('-26'); + }); + + it('displays the correct value when NULL values', () => { + const rows = getContentItems(fields, xField, [2, 2, null], null, TooltipDisplayMode.Multi, SortOrder.Descending); + expect(rows.length).toBe(1); + expect(rows[0].value).toBe('70'); + }); + }); + + describe('it tests getContentItems with string values', () => { + const timeValues = [1707833954056, 1707838274056, 1707842594056]; + const seriesAValues = ['LOW', 'HIGH', 'NORMAL']; + const seriesBValues = ['NORMAL', 'LOW', 'LOW']; + + const frame = { + name: 'a', + length: timeValues.length, + fields: [ + { + name: 'time', + type: FieldType.time, + values: timeValues[0], + config: {}, + display: (value: string) => ({ + text: value, + color: undefined, + numeric: NaN, + }), + }, + { + name: 'A-series', + type: FieldType.string, + values: seriesAValues, + config: {}, + display: (value: string) => ({ + text: value, + color: undefined, + numeric: NaN, + }), + }, + { + name: 'B-series', + type: FieldType.string, + values: seriesBValues, + config: {}, + display: (value: string) => ({ + text: value, + color: undefined, + numeric: NaN, + }), + }, + ], + } as unknown as DataFrame; + + const fields = frame.fields; + const xField = frame.fields[0]; + const dataIdxs = [null, 0, 0]; + + it('displays one series in single mode', () => { + const rows = getContentItems(fields, xField, [null, null, 0], 2, TooltipDisplayMode.Single, SortOrder.None); + expect(rows.length).toBe(1); + expect(rows[0].value).toBe('NORMAL'); + }); + + it('displays the right content in multi mode', () => { + const rows = getContentItems(fields, xField, dataIdxs, 2, TooltipDisplayMode.Multi, SortOrder.None); + expect(rows.length).toBe(2); + expect(rows[0].value).toBe('LOW'); + expect(rows[1].value).toBe('NORMAL'); + }); + + it('displays the values sorted ASC', () => { + const rows = getContentItems(fields, xField, dataIdxs, 2, TooltipDisplayMode.Multi, SortOrder.Ascending); + expect(rows.length).toBe(2); + expect(rows[0].value).toBe('LOW'); + expect(rows[1].value).toBe('NORMAL'); + }); + + it('displays the values sorted DESC', () => { + const rows = getContentItems( + frame.fields, + frame.fields[0], + dataIdxs, + 2, + TooltipDisplayMode.Multi, + SortOrder.Descending + ); + expect(rows.length).toBe(2); + expect(rows[0].value).toBe('NORMAL'); + expect(rows[1].value).toBe('LOW'); + }); + }); }); diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 6a229a3f8c3..24d81a57c40 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -1,5 +1,8 @@ +import { FALLBACK_COLOR, Field, FieldType, formattedValueToString } from '@grafana/data'; +import { SortOrder, TooltipDisplayMode } from '@grafana/schema'; + import { ColorIndicatorStyles } from './VizTooltipColorIndicator'; -import { ColorIndicator } from './types'; +import { ColorIndicator, ColorPlacement, LabelValue } from './types'; export const calculateTooltipPosition = ( xPos = 0, @@ -66,3 +69,80 @@ export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndi return styles.value; } }; + +const numberCmp = (a: LabelValue, b: LabelValue) => a.numeric! - b.numeric!; +const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); +const stringCmp = (a: LabelValue, b: LabelValue) => collator.compare(`${a.value}`, `${b.value}`); + +export const getContentItems = ( + fields: Field[], + xField: Field, + dataIdxs: Array, + seriesIdx: number | null | undefined, + mode: TooltipDisplayMode, + sortOrder: SortOrder, + fieldFilter = (field: Field) => true +): LabelValue[] => { + let rows: LabelValue[] = []; + + let allNumeric = false; + + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + + if ( + field === xField || + field.type === FieldType.time || + !fieldFilter(field) || + field.config.custom?.hideFrom?.tooltip || + field.config.custom?.hideFrom?.viz + ) { + continue; + } + + // in single mode, skip all but closest field + if (mode === TooltipDisplayMode.Single && seriesIdx !== i) { + continue; + } + + let dataIdx = dataIdxs[i]; + + // omit non-hovered + if (dataIdx == null) { + continue; + } + + if (!(field.type === FieldType.number || field.type === FieldType.boolean || field.type === FieldType.enum)) { + allNumeric = false; + } + + const v = fields[i].values[dataIdx]; + + // no value -> zero? + const display = field.display!(v); // super expensive :( + // sort NaN and non-numeric to bottom (regardless of sort order) + const numeric = !Number.isNaN(display.numeric) + ? display.numeric + : sortOrder === SortOrder.Descending + ? Number.MIN_SAFE_INTEGER + : Number.MAX_SAFE_INTEGER; + + rows.push({ + label: field.state?.displayName ?? field.name, + value: formattedValueToString(display), + color: display.color ?? FALLBACK_COLOR, + colorIndicator: ColorIndicator.series, + colorPlacement: ColorPlacement.first, + isActive: mode === TooltipDisplayMode.Multi && seriesIdx === i, + numeric, + }); + } + + if (sortOrder !== SortOrder.None && rows.length > 1) { + const cmp = allNumeric ? numberCmp : stringCmp; + const mult = sortOrder === SortOrder.Descending ? -1 : 1; + rows.sort((a, b) => mult * cmp(a, b)); + } + + return rows; +}; diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index b1ad4f08b7a..524296eefba 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; -import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema'; +import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode } from '@grafana/schema'; import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; @@ -58,6 +58,8 @@ export class TimelineChart extends React.Component { // When there is only one row, use the full space rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1, getValueColor: this.getValueColor, + // @ts-ignore + hoverMulti: this.props.tooltip.mode === TooltipDisplayMode.Multi, }); }; diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index 7d7dc8406d2..e5fcb3555c8 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -55,6 +55,7 @@ export interface TimelineCoreOptions { getFieldConfig: (seriesIdx: number) => StateTimeLineFieldConfig | StatusHistoryFieldConfig; onHover: (seriesIdx: number, valueIdx: number, rect: Rect) => void; onLeave: () => void; + hoverMulti: boolean; } /** @@ -79,6 +80,7 @@ export function getConfig(opts: TimelineCoreOptions) { getFieldConfig, onHover, onLeave, + hoverMulti, } = opts; let qt: Quadtree; @@ -405,8 +407,6 @@ export function getConfig(opts: TimelineCoreOptions) { } } - const hoverMulti = mode === TimelineMode.Changes; - const cursor: uPlot.Cursor = { x: mode === TimelineMode.Changes, y: false, diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index ef52d51c94a..6aab7e2478e 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -63,6 +63,7 @@ interface UPlotConfigOptions { getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string; // Identifies the shared key for uPlot cursor sync eventsScope?: string; + hoverMulti: boolean; } /** @@ -105,6 +106,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( mergeValues, getValueColor, eventsScope = '__global_', + hoverMulti, }) => { const builder = new UPlotConfigBuilder(timeZones[0]); @@ -165,6 +167,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( hoveredDataIdx = null; shouldChangeHover = true; }, + hoverMulti, }; let shouldChangeHover = false; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 71f4b4521fa..2e1d6ab208e 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -297,6 +297,7 @@ export const CandlestickPanel = ({ dataIdxs={dataIdxs} seriesIdx={seriesIdx} mode={options.tooltip.mode} + sortOrder={options.tooltip.sort} isPinned={isPinned} annotate={enableAnnotationCreation ? annotate : undefined} scrollable={isTooltipScrollable(options.tooltip)} diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index 469d07181d4..ff8d3c2514e 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/css'; import React, { ReactElement, useEffect, useRef, useState } from 'react'; import uPlot from 'uplot'; @@ -9,7 +8,6 @@ import { formattedValueToString, getFieldDisplayName, getLinksSupplier, - GrafanaTheme2, InterpolateFunction, LinkModel, PanelData, @@ -26,6 +24,8 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; +import { getStyles } from '../timeseries/TimeSeriesTooltip'; + import { HeatmapData } from './fields'; import { renderHistogram } from './renderHistogram'; import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils'; @@ -112,7 +112,7 @@ const HeatmapHoverCell = ({ let nonNumericOrdinalDisplay: string | undefined = undefined; - let contentLabelValue: LabelValue[] = []; + let contentItems: LabelValue[] = []; const getYValueIndex = (idx: number) => { return idx % data.yBucketCount! ?? 0; @@ -244,7 +244,7 @@ const HeatmapHoverCell = ({ if (mode === TooltipDisplayMode.Single || isPinned) { const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : []; - contentLabelValue = [ + contentItems = [ { label: getFieldDisplayName(countField, data.heatmap), value: data.display!(count), @@ -272,7 +272,7 @@ const HeatmapHoverCell = ({ const vals: LabelValue[] = getDisplayData(fromIdx, toIdx); vals.forEach((val) => { - contentLabelValue.push({ + contentItems.push({ label: val.label, value: val.value, color: val.color ?? '#FFF', @@ -330,26 +330,17 @@ const HeatmapHoverCell = ({ [index] ); - const getHeaderLabel = (): LabelValue => { - return { - label: '', - value: xDisp(xBucketMax)!, - }; + const headerLabel: LabelValue = { + label: '', + value: xDisp(xBucketMax!)!, }; - const getContentLabelValue = (): LabelValue[] => { - return contentLabelValue; - }; - - const getCustomContent = () => { - let content: ReactElement[] = []; - if (mode !== TooltipDisplayMode.Single) { - return content; - } + let customContent: ReactElement[] = []; + if (mode === TooltipDisplayMode.Single) { // Histogram - if (showHistogram) { - content.push( + if (showHistogram && !isSparse) { + customContent.push( ); } - - return content; - }; + } const styles = useStyles2(getStyles); return (
- - + + {isPinned && }
); }; - -const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css({ - display: 'flex', - flexDirection: 'column', - }), -}); diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 760fb13de80..e5b0cb933c2 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -411,7 +411,7 @@ export const plugin = new PanelPlugin(HeatmapPanel) name: 'Show histogram (Y axis)', defaultValue: defaultOptions.tooltip.yHistogram, category, - showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None, + showIf: (opts) => opts.tooltip.mode === TooltipDisplayMode.Single, }); builder.addBooleanSwitch({ @@ -419,7 +419,7 @@ export const plugin = new PanelPlugin(HeatmapPanel) name: 'Show color scale', defaultValue: defaultOptions.tooltip.showColorScale, category, - showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None && config.featureToggles.newVizTooltips, + showIf: (opts) => opts.tooltip.mode === TooltipDisplayMode.Single && config.featureToggles.newVizTooltips, }); builder.addNumberInput({ diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index b6418ddc219..cf3539fd4d2 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -224,16 +224,16 @@ export const StateTimelinePanel = ({ return ( ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index 086c790e73c..848b2ea0a3a 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -1,102 +1,58 @@ -import { css } from '@emotion/css'; import React from 'react'; -import { - arrayUtils, - DataFrame, - Field, - FieldType, - getDisplayProcessor, - getFieldDisplayName, - GrafanaTheme2, - LinkModel, - TimeRange, - TimeZone, -} from '@grafana/data'; +import { Field, FieldType, getFieldDisplayName, LinkModel, TimeRange } from '@grafana/data'; import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen'; -import { TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; +import { TooltipDisplayMode, useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils'; import { getDataLinks } from '../status-history/utils'; +import { TimeSeriesTooltipProps, getStyles } from '../timeseries/TimeSeriesTooltip'; -interface StateTimelineTooltip2Props { - data: DataFrame[]; - alignedData: DataFrame; - dataIdxs: Array; - seriesIdx: number | null | undefined; - isPinned: boolean; - timeZone?: TimeZone; +interface StateTimelineTooltip2Props extends TimeSeriesTooltipProps { timeRange: TimeRange; - mode?: TooltipDisplayMode; - sortOrder?: SortOrder; - annotate?: () => void; + withDuration: boolean; } export const StateTimelineTooltip2 = ({ - data, - alignedData, + frames, + seriesFrame, dataIdxs, seriesIdx, - timeZone, - timeRange, mode = TooltipDisplayMode.Single, sortOrder = SortOrder.None, + scrollable = false, isPinned, annotate, + timeRange, + withDuration, }: StateTimelineTooltip2Props) => { const styles = useStyles2(getStyles); - const theme = useTheme2(); - const datapointIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null); + const xField = seriesFrame.fields[0]; - if (datapointIdx == null || seriesIdx == null) { - return null; - } + const dataIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null); - const valueFieldsCount = data.reduce( - (acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length, - 0 - ); + const xVal = xField.display!(xField.values[dataIdx!]).text; - /** - * There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first - * from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count. - * Render nothing in this case to prevent error. - * See https://github.com/grafana/support-escalations/issues/932 - */ - if (alignedData.fields.length - 1 !== valueFieldsCount || !alignedData.fields[seriesIdx]) { - return null; - } + mode = isPinned ? TooltipDisplayMode.Single : mode; - let contentLabelValue: LabelValue[] = []; + const contentItems = getContentItems(seriesFrame.fields, xField, dataIdxs, seriesIdx, mode, sortOrder); - const xField = alignedData.fields[0]; - const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); - - let links: Array> = []; - - const from = xFieldFmt(xField.values[datapointIdx!]).text; - - // Single mode - if (mode === TooltipDisplayMode.Single || isPinned) { - const field = alignedData.fields[seriesIdx!]; - links = getDataLinks(field, datapointIdx); - - const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const value = field.values[datapointIdx!]; - const display = fieldFmt(value); - - const nextStateIdx = findNextStateIndex(field, datapointIdx!); + // append duration in single mode + if (withDuration && mode === TooltipDisplayMode.Single) { + const field = seriesFrame.fields[seriesIdx!]; + const nextStateIdx = findNextStateIndex(field, dataIdx!); let nextStateTs; if (nextStateIdx) { nextStateTs = xField.values[nextStateIdx!]; } - const stateTs = xField.values[datapointIdx!]; + const stateTs = xField.values[dataIdx!]; let duration: string; if (nextStateTs) { @@ -106,88 +62,29 @@ export const StateTimelineTooltip2 = ({ duration = fmtDuration(to - stateTs); } - const durationEntry: LabelValue[] = duration ? [{ label: 'Duration', value: duration }] : []; - - contentLabelValue = [ - { - label: getFieldDisplayName(field), - value: display.text, - color: display.color, - colorIndicator: ColorIndicator.value, - colorPlacement: ColorPlacement.trailing, - }, - ...durationEntry, - ]; + contentItems.push({ label: 'Duration', value: duration }); } - if (mode === TooltipDisplayMode.Multi && !isPinned) { - const fields = alignedData.fields; - const sortIdx: unknown[] = []; + let links: Array> = []; - for (let i = 0; i < fields.length; i++) { - const field = fields[i]; - if ( - !field || - field === xField || - field.type === FieldType.time || - field.config.custom?.hideFrom?.tooltip || - field.config.custom?.hideFrom?.viz - ) { - continue; - } - - const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const v = field.values[dataIdxs[i]!]; - const display = fieldFmt(v); - - sortIdx.push(v); - contentLabelValue.push({ - label: getFieldDisplayName(field), - value: display.text, - color: display.color, - colorIndicator: ColorIndicator.value, - colorPlacement: ColorPlacement.trailing, - isActive: seriesIdx === i, - }); - } - - if (sortOrder !== SortOrder.None) { - // create sort reference series array, as Array.sort() mutates the original array - const sortRef = [...contentLabelValue]; - const sortFn = arrayUtils.sortValues(sortOrder); - - contentLabelValue.sort((a, b) => { - // get compared values indices to retrieve raw values from sortIdx - const aIdx = sortRef.indexOf(a); - const bIdx = sortRef.indexOf(b); - return sortFn(sortIdx[aIdx], sortIdx[bIdx]); - }); - } + if (seriesIdx != null) { + const field = seriesFrame.fields[seriesIdx]; + const dataIdx = dataIdxs[seriesIdx]!; + links = getDataLinks(field, dataIdx); } - const getHeaderLabel = (): LabelValue => { - return { - label: '', - value: from, - }; - }; - - const getContentLabelValue = (): LabelValue[] => { - return contentLabelValue; + const headerItem: LabelValue = { + label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames), + value: xVal, }; return ( -
- - - {isPinned && } +
+
+ + + {isPinned && } +
); }; - -const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css({ - display: 'flex', - flexDirection: 'column', - }), -}); diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 541e839c851..241e0f6889d 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -22,13 +22,13 @@ import { TimelineMode, } from 'app/core/components/TimelineChart/utils'; +import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2'; import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { getTimezones } from '../timeseries/utils'; import { StatusHistoryTooltip } from './StatusHistoryTooltip'; -import { StatusHistoryTooltip2 } from './StatusHistoryTooltip2'; import { Options } from './panelcfg.gen'; const TOOLTIP_OFFSET = 10; @@ -251,16 +251,17 @@ export const StatusHistoryPanel = ({ }; return ( - ); }} diff --git a/public/app/plugins/panel/status-history/StatusHistoryTooltip2.tsx b/public/app/plugins/panel/status-history/StatusHistoryTooltip2.tsx deleted file mode 100644 index a58075a9e0f..00000000000 --- a/public/app/plugins/panel/status-history/StatusHistoryTooltip2.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { - DataFrame, - Field, - formattedValueToString, - getDisplayProcessor, - getFieldDisplayName, - GrafanaTheme2, - TimeZone, - LinkModel, - FieldType, - arrayUtils, -} from '@grafana/data'; -import { SortOrder, TooltipDisplayMode } from '@grafana/schema'; -import { useStyles2 } from '@grafana/ui'; -import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; -import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; -import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; - -import { getDataLinks } from './utils'; - -interface StatusHistoryTooltipProps { - data: DataFrame[]; - dataIdxs: Array; - alignedData: DataFrame; - seriesIdx: number | null | undefined; - timeZone: TimeZone; - isPinned: boolean; - mode?: TooltipDisplayMode; - sortOrder?: SortOrder; - annotate?: () => void; -} - -function fmt(field: Field, val: number): string { - if (field.display) { - return formattedValueToString(field.display(val)); - } - - return `${val}`; -} - -export const StatusHistoryTooltip2 = ({ - dataIdxs, - alignedData, - seriesIdx, - mode = TooltipDisplayMode.Single, - sortOrder = SortOrder.None, - isPinned, - annotate, -}: StatusHistoryTooltipProps) => { - const styles = useStyles2(getStyles); - - const datapointIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null); - - if (datapointIdx == null || seriesIdx == null) { - return null; - } - - let contentLabelValue: LabelValue[] = []; - - const xField = alignedData.fields[0]; - let links: Array> = []; - - // Single mode - if (mode === TooltipDisplayMode.Single || isPinned) { - const field = alignedData.fields[seriesIdx!]; - links = getDataLinks(field, datapointIdx); - - const fieldFmt = field.display || getDisplayProcessor(); - const value = field.values[datapointIdx!]; - const display = fieldFmt(value); - - contentLabelValue = [ - { - label: getFieldDisplayName(field), - value: fmt(field, field.values[datapointIdx]), - color: display.color, - colorIndicator: ColorIndicator.value, - colorPlacement: ColorPlacement.trailing, - }, - ]; - } - - if (mode === TooltipDisplayMode.Multi && !isPinned) { - const frame = alignedData; - const fields = frame.fields; - const sortIdx: unknown[] = []; - - for (let i = 0; i < fields.length; i++) { - const field = frame.fields[i]; - if ( - !field || - field === xField || - field.type === FieldType.time || - field.config.custom?.hideFrom?.tooltip || - field.config.custom?.hideFrom?.viz - ) { - continue; - } - - const fieldFmt = field.display || getDisplayProcessor(); - const v = field.values[datapointIdx!]; - const display = fieldFmt(v); - - sortIdx.push(v); - contentLabelValue.push({ - label: getFieldDisplayName(field), - value: fmt(field, field.values[datapointIdx]), - color: display.color, - colorIndicator: ColorIndicator.value, - colorPlacement: ColorPlacement.trailing, - isActive: seriesIdx === i, - }); - } - - if (sortOrder !== SortOrder.None) { - // create sort reference series array, as Array.sort() mutates the original array - const sortRef = [...contentLabelValue]; - const sortFn = arrayUtils.sortValues(sortOrder); - - contentLabelValue.sort((a, b) => { - // get compared values indices to retrieve raw values from sortIdx - const aIdx = sortRef.indexOf(a); - const bIdx = sortRef.indexOf(b); - return sortFn(sortIdx[aIdx], sortIdx[bIdx]); - }); - } - } - - const getHeaderLabel = (): LabelValue => { - return { - label: '', - value: fmt(xField, xField.values[datapointIdx]), - }; - }; - - const getContentLabelValue = () => { - return contentLabelValue; - }; - - return ( -
- - - {isPinned && } -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css({ - display: 'flex', - flexDirection: 'column', - }), -}); diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 8c62a4ac6bc..c7d9461bef8 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -1,31 +1,21 @@ import { css } from '@emotion/css'; import React from 'react'; -import { - DataFrame, - FALLBACK_COLOR, - FieldType, - GrafanaTheme2, - formattedValueToString, - getDisplayProcessor, - LinkModel, - Field, - getFieldDisplayName, - arrayUtils, -} from '@grafana/data'; +import { DataFrame, FieldType, LinkModel, Field, getFieldDisplayName } from '@grafana/data'; import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; -import { useStyles2, useTheme2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { getDataLinks } from '../status-history/utils'; // exemplar / annotation / time region hovering? // add annotation UI / alert dismiss UI? -interface TimeSeriesTooltipProps { +export interface TimeSeriesTooltipProps { frames?: DataFrame[]; // aligned series frame seriesFrame: DataFrame; @@ -53,119 +43,47 @@ export const TimeSeriesTooltip = ({ isPinned, annotate, }: TimeSeriesTooltipProps) => { - const theme = useTheme2(); const styles = useStyles2(getStyles); const xField = seriesFrame.fields[0]; - if (!xField) { - return null; - } - const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, theme }); - let xVal = xFieldFmt(xField!.values[dataIdxs[0]!]).text; + const xVal = xField.display!(xField.values[dataIdxs[0]!]).text; + + const contentItems = getContentItems( + seriesFrame.fields, + xField, + dataIdxs, + seriesIdx, + mode, + sortOrder, + (field) => field.type === FieldType.number + ); let links: Array> = []; - let contentLabelValue: LabelValue[] = []; - - // Single mode - if (mode === TooltipDisplayMode.Single) { - const field = seriesFrame.fields[seriesIdx!]; - if (!field) { - return null; - } - - const dataIdx = dataIdxs[seriesIdx!]!; - xVal = xFieldFmt(xField!.values[dataIdx]).text; - const fieldFmt = field.display || getDisplayProcessor({ field, theme }); - const display = fieldFmt(field.values[dataIdx]); + if (seriesIdx != null) { + const field = seriesFrame.fields[seriesIdx]; + const dataIdx = dataIdxs[seriesIdx]!; links = getDataLinks(field, dataIdx); - - contentLabelValue = [ - { - label: getFieldDisplayName(field, seriesFrame, frames), - value: display ? formattedValueToString(display) : null, - color: display.color || FALLBACK_COLOR, - colorIndicator: ColorIndicator.series, - colorPlacement: ColorPlacement.first, - }, - ]; } - if (mode === TooltipDisplayMode.Multi) { - const fields = seriesFrame.fields; - const sortIdx: unknown[] = []; - - for (let i = 0; i < fields.length; i++) { - const field = seriesFrame.fields[i]; - if ( - !field || - field === xField || - field.type === FieldType.time || - field.type !== FieldType.number || - field.config.custom?.hideFrom?.tooltip || - field.config.custom?.hideFrom?.viz - ) { - continue; - } - - const v = seriesFrame.fields[i].values[dataIdxs[i]!]; - const display = field.display!(v); // super expensive :( - - sortIdx.push(v); - contentLabelValue.push({ - label: field.state?.displayName ?? field.name, - value: display ? formattedValueToString(display) : null, - color: display.color || FALLBACK_COLOR, - colorIndicator: ColorIndicator.series, - colorPlacement: ColorPlacement.first, - isActive: seriesIdx === i, - }); - - if (sortOrder !== SortOrder.None) { - // create sort reference series array, as Array.sort() mutates the original array - const sortRef = [...contentLabelValue]; - const sortFn = arrayUtils.sortValues(sortOrder); - - contentLabelValue.sort((a, b) => { - // get compared values indices to retrieve raw values from sortIdx - const aIdx = sortRef.indexOf(a); - const bIdx = sortRef.indexOf(b); - return sortFn(sortIdx[aIdx], sortIdx[bIdx]); - }); - } - } - - if (seriesIdx != null) { - const field = seriesFrame.fields[seriesIdx]; - const dataIdx = dataIdxs[seriesIdx]!; - links = getDataLinks(field, dataIdx); - } - } - - const getHeaderLabel = (): LabelValue => { - return { - label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames), - value: xVal, - }; - }; - - const getContentLabelValue = () => { - return contentLabelValue; + const headerItem: LabelValue = { + label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames), + value: xVal, }; return (
- - + + {isPinned && }
); }; -const getStyles = (theme: GrafanaTheme2) => ({ +export const getStyles = () => ({ wrapper: css({ display: 'flex', flexDirection: 'column', diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 3a79fd6ba21..00243d1c326 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -1,7 +1,6 @@ -import { css } from '@emotion/css'; import React from 'react'; -import { DataFrame, Field, getFieldDisplayName, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { DataFrame, Field, getFieldDisplayName, LinkModel } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; @@ -10,8 +9,10 @@ import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizToolt import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; import { getTitleFromHref } from 'app/features/explore/utils/links'; +import { getStyles } from '../timeseries/TimeSeriesTooltip'; + import { Options } from './panelcfg.gen'; -import { ScatterSeries, YValue } from './types'; +import { ScatterSeries } from './types'; import { fmt } from './utils'; export interface Props { @@ -41,66 +42,46 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, const xField = series.x(frame); const yField = series.y(frame); - const getHeaderLabel = (): LabelValue => { - let label = series.name; - if (options.seriesMapping === 'manual') { - label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`; - } + let label = series.name; + if (options.seriesMapping === 'manual') { + label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`; + } - let colorThing = series.pointColor(frame); + let colorThing = series.pointColor(frame); - if (Array.isArray(colorThing)) { - colorThing = colorThing[rowIndex]; - } + if (Array.isArray(colorThing)) { + colorThing = colorThing[rowIndex]; + } - return { - label, - value: null, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - color: alpha(colorThing as string, 0.5), - colorIndicator: ColorIndicator.marker_md, - }; + const headerItem: LabelValue = { + label, + value: '', + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + color: alpha(colorThing as string, 0.5), + colorIndicator: ColorIndicator.marker_md, }; - const getContentLabel = (): LabelValue[] => { - let colorThing = series.pointColor(frame); + const contentItems: LabelValue[] = [ + { + label: getFieldDisplayName(xField, frame), + value: fmt(xField, xField.values[rowIndex]), + }, + { + label: getFieldDisplayName(yField, frame), + value: fmt(yField, yField.values[rowIndex]), + }, + ]; - if (Array.isArray(colorThing)) { - colorThing = colorThing[rowIndex]; - } - - const yValue: YValue = { - name: getFieldDisplayName(yField, frame), - val: yField.values[rowIndex], - field: yField, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - color: alpha(colorThing as string, 0.5), - }; - - const content: LabelValue[] = [ - { - label: getFieldDisplayName(xField, frame), - value: fmt(xField, xField.values[rowIndex]), - }, - { - label: yValue.name, - value: fmt(yValue.field, yValue.val), - }, - ]; - - // add extra fields - const extraFields: Field[] = frame.fields.filter((f) => f !== xField && f !== yField); - if (extraFields) { - extraFields.forEach((field) => { - content.push({ - label: field.name, - value: fmt(field, field.values[rowIndex]), - }); + // add extra fields + const extraFields: Field[] = frame.fields.filter((f) => f !== xField && f !== yField); + if (extraFields) { + extraFields.forEach((field) => { + contentItems.push({ + label: field.name, + value: fmt(field, field.values[rowIndex]), }); - } - - return content; - }; + }); + } const getLinks = (): Array> => { let links: Array> = []; @@ -120,16 +101,9 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, return (
- - + + {isPinned && }
); }; - -const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css({ - display: 'flex', - flexDirection: 'column', - }), -}); diff --git a/public/app/plugins/panel/xychart/types.ts b/public/app/plugins/panel/xychart/types.ts index ca9d9ddfb1d..fa329c1ee49 100644 --- a/public/app/plugins/panel/xychart/types.ts +++ b/public/app/plugins/panel/xychart/types.ts @@ -51,13 +51,6 @@ export interface ScatterSeries { }; } -export interface YValue { - name: string; - val: number; - field: Field; - color: string; -} - export interface ExtraFacets { colorFacetFieldName: string; sizeFacetFieldName: string;