From d4b75928ca87a3419d6183e00f30e1d96aff2eba Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:34:56 -0600 Subject: [PATCH] Tooltip: Improved Timeseries and Candlestick tooltips (#75841) --- .../VizTooltip/VizTooltipColorIndicator.tsx | 13 +- .../VizTooltip/VizTooltipContent.tsx | 3 +- .../VizTooltip/VizTooltipHeaderLabelValue.tsx | 1 - .../components/VizTooltip/VizTooltipRow.tsx | 16 +- .../src/components/VizTooltip/types.ts | 3 + .../uPlot/plugins/TooltipPlugin2.tsx | 2 + .../panel/candlestick/CandlestickPanel.tsx | 139 +++++++++------ .../panel/timeseries/TimeSeriesPanel.tsx | 148 +++++++++------- .../panel/timeseries/TimeSeriesTooltip.tsx | 162 ++++++++++++++++++ .../app/plugins/panel/trend/TrendTooltip.tsx | 3 +- 10 files changed, 360 insertions(+), 130 deletions(-) create mode 100644 public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx index bf6d5554ff5..39e9c495f03 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx @@ -1,21 +1,24 @@ import { css, cx } from '@emotion/css'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { FALLBACK_COLOR, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; -import { ColorIndicator } from './types'; +import { ColorIndicator, DEFAULT_COLOR_INDICATOR } from './types'; import { getColorIndicatorClass } from './utils'; interface Props { - color: string; - colorIndicator: ColorIndicator; + color?: string; + colorIndicator?: ColorIndicator; } export type ColorIndicatorStyles = ReturnType; -export const VizTooltipColorIndicator = ({ color, colorIndicator = ColorIndicator.value }: Props) => { +export const VizTooltipColorIndicator = ({ + color = FALLBACK_COLOR, + colorIndicator = DEFAULT_COLOR_INDICATOR, +}: Props) => { const styles = useStyles2(getStyles); return ( diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index b76177b2e90..f131472b71a 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -19,7 +19,7 @@ export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) = return (
- {contentLabelValue?.map((labelValue, i) => { + {contentLabelValue.map((labelValue, i) => { const { label, value, color, colorIndicator, colorPlacement, isActive } = labelValue; return ( diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx index b5efda32c6f..dcbf02a9037 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx @@ -16,7 +16,6 @@ export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => ( value={keyValuePair.value} color={keyValuePair.color} colorIndicator={keyValuePair.colorIndicator!} - colorFirst={false} justify={'space-between'} /> ))} diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx index 9fdd2d051cd..bcdc1ceac19 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx @@ -11,7 +11,6 @@ import { ColorPlacement, LabelValue } from './types'; interface Props extends LabelValue { justify?: string; - colorFirst?: boolean; isActive?: boolean; // for series list marginRight?: string; } @@ -21,9 +20,8 @@ export const VizTooltipRow = ({ value, color, colorIndicator, - colorPlacement = ColorPlacement.leading, + colorPlacement = ColorPlacement.first, justify = 'flex-start', - colorFirst = true, isActive = false, marginRight = '0px', }: Props) => { @@ -52,7 +50,9 @@ export const VizTooltipRow = ({
{(color || label) && (
- {color && colorFirst && } + {color && colorPlacement === ColorPlacement.first && ( + + )}
- {color && !colorFirst && colorPlacement === ColorPlacement.leading && ( - + {color && colorPlacement === ColorPlacement.leading && ( + )}
{value}
- {color && !colorFirst && colorPlacement === ColorPlacement.trailing && ( + {color && colorPlacement === ColorPlacement.trailing && ( <>   - + )}
diff --git a/packages/grafana-ui/src/components/VizTooltip/types.ts b/packages/grafana-ui/src/components/VizTooltip/types.ts index 80591ca9eab..4d33d3c2637 100644 --- a/packages/grafana-ui/src/components/VizTooltip/types.ts +++ b/packages/grafana-ui/src/components/VizTooltip/types.ts @@ -12,6 +12,7 @@ export enum ColorIndicator { export enum ColorPlacement { hidden = 'hidden', + first = 'first', leading = 'leading', trailing = 'trailing', } @@ -24,3 +25,5 @@ export interface LabelValue { colorPlacement?: ColorPlacement; isActive?: boolean; } + +export const DEFAULT_COLOR_INDICATOR = ColorIndicator.series; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 7f15437dcb3..01e0258be9d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -10,6 +10,8 @@ import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; import { CloseButton } from './CloseButton'; +export const DEFAULT_TOOLTIP_WIDTH = 280; + // todo: barchart? histogram? export const enum TooltipHoverMode { // Single mode in TimeSeries, Candlestick, Trend, StateTimeline, Heatmap? diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index a0396c4a01b..10b0746ddc4 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -7,12 +7,14 @@ import uPlot from 'uplot'; import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; +import { TooltipPlugin, TooltipPlugin2, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; +import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; +import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; @@ -242,7 +244,7 @@ export const CandlestickPanel = ({ tweakScale={tweakScale} options={options} > - {(config, alignedDataFrame) => { + {(uplotConfig, alignedDataFrame) => { alignedDataFrame.fields.forEach((field) => { field.getLinks = getLinksSupplier( alignedDataFrame, @@ -255,73 +257,100 @@ export const CandlestickPanel = ({ return ( <> - - - {/* Renders annotation markers*/} - {data.annotations && ( - - )} - {/* Enables annotations creation*/} - {enableAnnotationCreation ? ( - - {({ startAnnotating }) => { + {config.featureToggles.newVizTooltips ? ( + { return ( - { - if (!p) { - return; - } - startAnnotating({ coords: p.coords }); - }, - }, - ], - }, - ] - : [] - } + ); }} - - ) : ( - + ) : ( + <> + + + + )} + {/* Renders annotation markers*/} + {data.annotations && ( + + )} + {/* Enables annotations creation*/} + {!config.featureToggles.newVizTooltips ? ( + enableAnnotationCreation ? ( + + {({ startAnnotating }) => { + return ( + { + if (!p) { + return; + } + startAnnotating({ coords: p.coords }); + }, + }, + ], + }, + ] + : [] + } + /> + ); + }} + + ) : ( + + ) + ) : undefined} + {data.annotations && ( + )} - {data.annotations && } {((canEditThresholds && onThresholdsChange) || showThresholds) && ( )} - + ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index f9d57f04578..7cb96db9f22 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -3,10 +3,12 @@ import React, { useMemo } from 'react'; import { PanelProps, DataFrameType } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { KeyboardPlugin, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; +import { KeyboardPlugin, TooltipPlugin, TooltipPlugin2, usePanelContext, ZoomPlugin } from '@grafana/ui'; +import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; +import { TimeSeriesTooltip } from './TimeSeriesTooltip'; import { Options } from './panelcfg.gen'; import { AnnotationEditorPlugin } from './plugins/AnnotationEditorPlugin'; import { AnnotationsPlugin } from './plugins/AnnotationsPlugin'; @@ -74,7 +76,7 @@ export const TimeSeriesPanel = ({ legend={options.legend} options={options} > - {(config, alignedDataFrame) => { + {(uplotConfig, alignedDataFrame) => { if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) { alignedDataFrame = regenerateLinksSupplier( alignedDataFrame, @@ -87,68 +89,98 @@ export const TimeSeriesPanel = ({ return ( <> - - + {options.tooltip.mode === TooltipDisplayMode.None || ( - + <> + {config.featureToggles.newVizTooltips ? ( + { + return ( + + ); + }} + /> + ) : ( + <> + + + + )} + )} {/* Renders annotation markers*/} {data.annotations && ( - + )} - {/* Enables annotations creation*/} - {enableAnnotationCreation ? ( - - {({ startAnnotating }) => { - return ( - { - if (!p) { - return; - } - startAnnotating({ coords: p.coords }); + {/*Enables annotations creation*/} + {!config.featureToggles.newVizTooltips ? ( + enableAnnotationCreation ? ( + + {({ startAnnotating }) => { + return ( + { + if (!p) { + return; + } + startAnnotating({ coords: p.coords }); + }, }, - }, - ], - }, - ]} - /> - ); - }} - - ) : ( - - )} + ], + }, + ]} + /> + ); + }} + + ) : ( + + ) + ) : undefined} {data.annotations && ( @@ -156,13 +188,13 @@ export const TimeSeriesPanel = ({ {((canEditThresholds && onThresholdsChange) || showThresholds) && ( )} - + ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx new file mode 100644 index 00000000000..491b887a921 --- /dev/null +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -0,0 +1,162 @@ +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 { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; +import { useStyles2, useTheme2 } 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 { DEFAULT_TOOLTIP_WIDTH } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; + +import { getDataLinks } from '../status-history/utils'; + +// exemplar / annotation / time region hovering? +// add annotation UI / alert dismiss UI? + +interface TimeSeriesTooltipProps { + frames?: DataFrame[]; + // aligned series frame + seriesFrame: DataFrame; + // hovered points + dataIdxs: Array; + // closest/hovered series + seriesIdx?: number | null; + mode?: TooltipDisplayMode; + sortOrder?: SortOrder; + + isPinned: boolean; +} + +export const TimeSeriesTooltip = ({ + frames, + seriesFrame, + dataIdxs, + seriesIdx, + mode = TooltipDisplayMode.Single, + sortOrder = SortOrder.None, + isPinned, +}: 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; + let links: Array> = []; + let contentLabelValue: LabelValue[] = []; + + // Single mode + if (mode === TooltipDisplayMode.Single || isPinned) { + 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]); + 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 && !isPinned) { + 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]); + }); + } + } + } + + const getHeaderLabel = (): LabelValue => { + return { + label: '', + value: xVal, + }; + }; + + const getContentLabelValue = () => { + return contentLabelValue; + }; + + return ( +
+
+ + + {isPinned && } +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css({ + display: 'flex', + flexDirection: 'column', + width: DEFAULT_TOOLTIP_WIDTH, + }), +}); diff --git a/public/app/plugins/panel/trend/TrendTooltip.tsx b/public/app/plugins/panel/trend/TrendTooltip.tsx index 4da29cd4f06..277fde8f0e0 100644 --- a/public/app/plugins/panel/trend/TrendTooltip.tsx +++ b/public/app/plugins/panel/trend/TrendTooltip.tsx @@ -20,6 +20,7 @@ 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'; +import { DEFAULT_TOOLTIP_WIDTH } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; interface TrendTooltipProps { frames?: DataFrame[]; @@ -168,6 +169,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css({ display: 'flex', flexDirection: 'column', - width: '280px', + width: DEFAULT_TOOLTIP_WIDTH, }), });