Tooltip: Improved Trend tooltip (#77251)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2023-11-27 12:12:24 -06:00 committed by GitHub
parent 5015b5b2b0
commit 4aea1107b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 213 additions and 13 deletions

View File

@ -3,7 +3,8 @@ import React, { useMemo } from 'react';
import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, PanelProps, TimeRange } from '@grafana/data'; import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, PanelProps, TimeRange } from '@grafana/data';
import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames';
import { config, PanelDataErrorView } from '@grafana/runtime'; 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 { XYFieldMatchers } from 'app/core/components/GraphNG/types';
import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; import { preparePlotFrame } from 'app/core/components/GraphNG/utils';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; 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 { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils';
import { TrendTooltip } from './TrendTooltip';
import { Options } from './panelcfg.gen'; import { Options } from './panelcfg.gen';
export const TrendPanel = ({ export const TrendPanel = ({
@ -106,7 +108,7 @@ export const TrendPanel = ({
options={options} options={options}
preparePlotFrame={preparePlotFrameTimeless} preparePlotFrame={preparePlotFrameTimeless}
> >
{(config, alignedDataFrame) => { {(uPlotConfig, alignedDataFrame) => {
if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) { if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
alignedDataFrame = regenerateLinksSupplier( alignedDataFrame = regenerateLinksSupplier(
alignedDataFrame, alignedDataFrame,
@ -119,17 +121,42 @@ export const TrendPanel = ({
return ( return (
<> <>
<KeyboardPlugin config={config} /> <KeyboardPlugin config={uPlotConfig} />
{options.tooltip.mode === TooltipDisplayMode.None || ( {options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin <>
frames={info.frames!} {config.featureToggles.newVizTooltips ? (
data={alignedDataFrame} <TooltipPlugin2
config={config} config={uPlotConfig}
mode={options.tooltip.mode} hoverMode={
sortOrder={options.tooltip.sort} options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
sync={sync} }
timeZone={timeZone} render={(u, dataIdxs, seriesIdx, isPinned = false) => {
/> return (
<TrendTooltip
frames={info.frames!}
data={alignedDataFrame}
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
sync={sync}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
isPinned={isPinned}
/>
);
}}
/>
) : (
<TooltipPlugin
frames={info.frames!}
data={alignedDataFrame}
config={uPlotConfig}
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
sync={sync}
timeZone={timeZone}
/>
)}
</>
)} )}
</> </>
); );

View File

@ -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<number | null>;
// 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<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
// 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 = (
<SeriesList
series={[
{
color: display.color || FALLBACK_COLOR,
label: getFieldDisplayName(field, data, frames),
value: display ? formattedValueToString(display) : null,
},
]}
/>
);
}
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 = <SeriesList series={series} />;
}
const getHeaderLabel = (): LabelValue => {
return {
label: getFieldDisplayName(xField, data),
value: xVal,
};
};
return (
<div>
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} customValueDisplay={tooltip} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
width: '280px',
}),
});