diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 59aee22776d..fa3c6d355be 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -3,7 +3,8 @@ import React, { useMemo } from 'react'; import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, PanelProps, TimeRange } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { config, PanelDataErrorView } from '@grafana/runtime'; -import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin, usePanelContext } from '@grafana/ui'; +import { KeyboardPlugin, TooltipDisplayMode, usePanelContext, TooltipPlugin, TooltipPlugin2 } from '@grafana/ui'; +import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; @@ -11,6 +12,7 @@ import { findFieldIndex } from 'app/features/dimensions'; import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils'; +import { TrendTooltip } from './TrendTooltip'; import { Options } from './panelcfg.gen'; export const TrendPanel = ({ @@ -106,7 +108,7 @@ export const TrendPanel = ({ options={options} preparePlotFrame={preparePlotFrameTimeless} > - {(config, alignedDataFrame) => { + {(uPlotConfig, alignedDataFrame) => { if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) { alignedDataFrame = regenerateLinksSupplier( alignedDataFrame, @@ -119,17 +121,42 @@ export const TrendPanel = ({ return ( <> - - {options.tooltip.mode === TooltipDisplayMode.None || ( - + + {options.tooltip.mode !== TooltipDisplayMode.None && ( + <> + {config.featureToggles.newVizTooltips ? ( + { + return ( + + ); + }} + /> + ) : ( + + )} + )} ); diff --git a/public/app/plugins/panel/trend/TrendTooltip.tsx b/public/app/plugins/panel/trend/TrendTooltip.tsx new file mode 100644 index 00000000000..4da29cd4f06 --- /dev/null +++ b/public/app/plugins/panel/trend/TrendTooltip.tsx @@ -0,0 +1,173 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { + arrayUtils, + DashboardCursorSync, + DataFrame, + FALLBACK_COLOR, + Field, + FieldType, + formattedValueToString, + getDisplayProcessor, + getFieldDisplayName, + GrafanaTheme2, + LinkModel, +} from '@grafana/data'; +import { TooltipDisplayMode, SortOrder } from '@grafana/schema'; +import { SeriesTableRowProps, useStyles2, useTheme2 } from '@grafana/ui'; +import { SeriesList } from '@grafana/ui/src/components/VizTooltip/SeriesList'; +import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; +import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; + +interface TrendTooltipProps { + frames?: DataFrame[]; + // aligned data frame + data: DataFrame; + // config: UPlotConfigBuilder; + mode?: TooltipDisplayMode; + sortOrder?: SortOrder; + sync?: () => DashboardCursorSync; + + // hovered points + dataIdxs: Array; + // closest/hovered series + seriesIdx: number | null; + isPinned: boolean; +} + +export const TrendTooltip = ({ + frames, + data, + mode = TooltipDisplayMode.Single, + sortOrder = SortOrder.None, + sync, + dataIdxs, + seriesIdx, + isPinned, +}: TrendTooltipProps) => { + const theme = useTheme2(); + const styles = useStyles2(getStyles); + + const xField = data.fields[0]; + if (!xField) { + return null; + } + + const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, theme }); + let xVal = xFieldFmt(xField!.values[dataIdxs[0]!]).text; + let tooltip: React.ReactNode = null; + + const links: Array> = []; + const linkLookup = new Set(); + + // Single mode + if (mode === TooltipDisplayMode.Single || isPinned) { + const field = data.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 (field.getLinks) { + const v = field.values[dataIdx]; + const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; + field.getLinks({ calculatedValue: disp, valueRowIndex: dataIdx }).forEach((link) => { + const key = `${link.title}/${link.href}`; + if (!linkLookup.has(key)) { + links.push(link); + linkLookup.add(key); + } + }); + } + + tooltip = ( + + ); + } + + if (mode === TooltipDisplayMode.Multi && !isPinned) { + let series: SeriesTableRowProps[] = []; + const frame = data; + 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.type !== FieldType.number || + field.config.custom?.hideFrom?.tooltip || + field.config.custom?.hideFrom?.viz + ) { + continue; + } + + const v = data.fields[i].values[dataIdxs[i]!]; + const display = field.display!(v); + + sortIdx.push(v); + series.push({ + color: display.color || FALLBACK_COLOR, + label: field.state?.displayName ?? field.name, + value: display ? formattedValueToString(display) : null, + isActive: seriesIdx === i, + }); + } + + if (sortOrder !== SortOrder.None) { + // create sort reference series array, as Array.sort() mutates the original array + const sortRef = [...series]; + const sortFn = arrayUtils.sortValues(sortOrder); + + series.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]); + }); + } + + tooltip = ; + } + + const getHeaderLabel = (): LabelValue => { + return { + label: getFieldDisplayName(xField, data), + value: xVal, + }; + }; + + return ( +
+
+ + {isPinned && } +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css({ + display: 'flex', + flexDirection: 'column', + width: '280px', + }), +});