diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 70de02fd701..0d8058fc531 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -85,7 +85,7 @@ export const getContentItems = ( ): VizTooltipItem[] => { let rows: VizTooltipItem[] = []; - let allNumeric = false; + let allNumeric = true; for (let i = 0; i < fields.length; i++) { const field = fields[i]; diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx index 4d92189204f..c7f4fcc1a5c 100644 --- a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx +++ b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx @@ -39,7 +39,7 @@ export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: ); })} - {links && ( + {links && links.length > 0 && (
{links.map((link, i) => ( diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx index 7ef653a2d5f..f7c73022063 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx @@ -9,9 +9,7 @@ import { getFieldDisplayName, LinkModel, TimeRange, - getLinksSupplier, InterpolateFunction, - ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; import { LinkButton, VerticalGroup } from '@grafana/ui'; @@ -19,6 +17,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 { getDataLinks } from '../status-history/utils'; + import { HeatmapData } from './fields'; import { renderHistogram } from './renderHistogram'; import { HeatmapHoverEvent } from './utils'; @@ -29,7 +29,6 @@ type Props = { showHistogram?: boolean; timeRange: TimeRange; replaceVars: InterpolateFunction; - scopedVars: ScopedVars[]; }; export const HeatmapHoverView = (props: Props) => { @@ -39,7 +38,7 @@ export const HeatmapHoverView = (props: Props) => { return ; }; -const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, replaceVars }: Props) => { +const HeatmapHoverCell = ({ data, hover, showHistogram = false }: Props) => { const index = hover.dataIdx; const [isSparse] = useState( @@ -70,7 +69,8 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl const meta = readHeatmapRowsCustomMeta(data.heatmap); const yDisp = yField?.display ? (v: string) => formattedValueToString(yField.display!(v)) : (v: string) => `${v}`; - const yValueIdx = index % data.yBucketCount! ?? 0; + const yValueIdx = index % (data.yBucketCount ?? 1); + const xValueIdx = Math.floor(index / (data.yBucketCount ?? 1)); let yBucketMin: string; let yBucketMax: string; @@ -126,33 +126,16 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl const count = countVals?.[index]; - const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); - const links: Array> = []; - const linkLookup = new Set(); + let links: Array> = []; - for (const field of visibleFields ?? []) { - const hasLinks = field.config.links && field.config.links.length > 0; + const linksField = data.series?.fields[yValueIdx + 1]; - if (hasLinks && data.heatmap) { - const appropriateScopedVars = scopedVars.find( - (scopedVar) => - scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay - ); + if (linksField != null) { + const visible = !Boolean(linksField.config.custom?.hideFrom?.tooltip); + const hasLinks = (linksField.config.links?.length ?? 0) > 0; - field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars); - } - - if (field.getLinks) { - const value = field.values[index]; - const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; - - field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); + if (visible && hasLinks) { + links = getDataLinks(linksField, xValueIdx); } } diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 018ddca15cc..b03bb3bd2ee 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -1,17 +1,7 @@ import { css } from '@emotion/css'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { - DashboardCursorSync, - DataFrame, - DataFrameType, - Field, - getLinksSupplier, - GrafanaTheme2, - PanelProps, - ScopedVars, - TimeRange, -} from '@grafana/data'; +import { DashboardCursorSync, DataFrameType, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data'; import { config, PanelDataErrorView } from '@grafana/runtime'; import { ScaleDistributionConfig } from '@grafana/schema'; import { @@ -34,8 +24,8 @@ import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/tra import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { ExemplarModalHeader } from './ExemplarModalHeader'; -import { HeatmapHoverView } from './HeatmapHoverView'; -import { HeatmapHoverView as HeatmapHoverViewOld } from './HeatmapHoverViewOld'; +import { HeatmapHoverView } from './HeatmapHoverViewOld'; +import { HeatmapTooltip } from './HeatmapTooltip'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; import { Options } from './types'; @@ -70,50 +60,26 @@ export const HeatmapPanel = ({ // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - // necessary for enabling datalinks in hover view - let scopedVarsFromRawData: ScopedVars[] = []; - for (const series of data.series) { - for (const field of series.fields) { - if (field.state?.scopedVars) { - scopedVarsFromRawData.push(field.state.scopedVars); - } - } - } - // ugh let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; - const getFieldLinksSupplier = useCallback( - (exemplars: DataFrame, field: Field) => { - return getLinksSupplier(exemplars, field, field.state?.scopedVars ?? {}, replaceVariables); - }, - [replaceVariables] - ); - const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]); const info = useMemo(() => { try { - return prepareHeatmapData( - data.series, - data.annotations, - options, - palette, - theme, - getFieldLinksSupplier, - replaceVariables - ); + return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables); } catch (ex) { return { warning: `${ex}` }; } - }, [data.series, data.annotations, options, palette, theme, getFieldLinksSupplier, replaceVariables]); + }, [data.series, data.annotations, options, palette, theme, replaceVariables]); const facets = useMemo(() => { let exemplarsXFacet: number[] | undefined = []; // "Time" field let exemplarsYFacet: Array = []; const meta = readHeatmapRowsCustomMeta(info.heatmap); + if (info.exemplars?.length) { exemplarsXFacet = info.exemplars?.fields[0].values; @@ -265,7 +231,7 @@ export const HeatmapPanel = ({ }; return ( - ); @@ -308,13 +272,12 @@ export const HeatmapPanel = ({ allowPointerEvents={isToolTipOpen.current} > {shouldDisplayCloseButton && } - )} diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx similarity index 85% rename from public/app/plugins/panel/heatmap/HeatmapHoverView.tsx rename to public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index 6d946e35dc9..ed104d63f2f 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect, useRef, useState } from 'react'; +import React, { ReactElement, useEffect, useRef, useState, ReactNode } from 'react'; import uPlot from 'uplot'; import { @@ -7,11 +7,8 @@ import { FieldType, formattedValueToString, getFieldDisplayName, - getLinksSupplier, - InterpolateFunction, LinkModel, PanelData, - ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; import { TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; @@ -24,13 +21,14 @@ 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 { getDataLinks } from '../status-history/utils'; import { getStyles } from '../timeseries/TimeSeriesTooltip'; import { HeatmapData } from './fields'; import { renderHistogram } from './renderHistogram'; import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils'; -interface Props { +interface HeatmapTooltipProps { mode: TooltipDisplayMode; dataIdxs: Array; seriesIdx: number | null | undefined; @@ -40,12 +38,10 @@ interface Props { isPinned: boolean; dismiss: () => void; panelData: PanelData; - replaceVars: InterpolateFunction; - scopedVars: ScopedVars[]; annotate?: () => void; } -export const HeatmapHoverView = (props: Props) => { +export const HeatmapTooltip = (props: HeatmapTooltipProps) => { if (props.seriesIdx === 2) { return ( { +}: HeatmapTooltipProps) => { const index = dataIdxs[1]!; const data = dataRef.current; @@ -114,11 +108,8 @@ const HeatmapHoverCell = ({ let contentItems: VizTooltipItem[] = []; - const getYValueIndex = (idx: number) => { - return idx % data.yBucketCount! ?? 0; - }; - - let yValueIdx = getYValueIndex(index); + const yValueIdx = index % (data.yBucketCount ?? 1); + const xValueIdx = Math.floor(index / (data.yBucketCount ?? 1)); const getData = (idx: number = index) => { if (meta.yOrdinalDisplay) { @@ -187,7 +178,6 @@ const HeatmapHoverCell = ({ if (isSparse) { ({ xBucketMin, xBucketMax, yBucketMin, yBucketMax } = getSparseCellMinMax(data!, idx)); } else { - yValueIdx = getYValueIndex(idx); getData(idx); } @@ -283,34 +273,23 @@ const HeatmapHoverCell = ({ }); } - const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); - const links: Array> = []; - const linkLookup = new Set(); + let footer: ReactNode; - for (const field of visibleFields ?? []) { - const hasLinks = field.config.links && field.config.links.length > 0; + if (isPinned) { + let links: Array> = []; - if (hasLinks && data.heatmap) { - const appropriateScopedVars = scopedVars.find( - (scopedVar) => - scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay - ); + const linksField = data.series?.fields[yValueIdx + 1]; - field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars); + if (linksField != null) { + const visible = !Boolean(linksField.config.custom?.hideFrom?.tooltip); + const hasLinks = (linksField.config.links?.length ?? 0) > 0; + + if (visible && hasLinks) { + links = getDataLinks(linksField, xValueIdx); + } } - if (field.getLinks) { - const value = field.values[index]; - const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; - - field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); - } + footer = ; } let can = useRef(null); @@ -377,9 +356,7 @@ const HeatmapHoverCell = ({
))} - {(links.length > 0 || isPinned) && ( - - )} + {footer} ); }; diff --git a/public/app/plugins/panel/heatmap/fields.ts b/public/app/plugins/panel/heatmap/fields.ts index 0529ec3fce4..e45b88ae033 100644 --- a/public/app/plugins/panel/heatmap/fields.ts +++ b/public/app/plugins/panel/heatmap/fields.ts @@ -6,12 +6,11 @@ import { FieldType, formattedValueToString, getDisplayProcessor, + getLinksSupplier, GrafanaTheme2, InterpolateFunction, - LinkModel, outerJoinDataFrames, ValueFormatter, - ValueLinkConfig, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { HeatmapCellLayout } from '@grafana/schema'; @@ -39,6 +38,8 @@ export interface HeatmapData { maxValue: number; }; + series?: DataFrame; // the joined single frame for nonNumericOrdinalY data links + exemplars?: DataFrame; // optionally linked exemplars exemplarColor?: string; @@ -70,8 +71,7 @@ export function prepareHeatmapData( options: Options, palette: string[], theme: GrafanaTheme2, - getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array>, - replaceVariables?: InterpolateFunction + replaceVariables: InterpolateFunction = (v) => v ): HeatmapData { if (!frames?.length) { return {}; @@ -81,11 +81,9 @@ export function prepareHeatmapData( const exemplars = annotations?.find((f) => f.name === 'exemplar'); - if (getFieldLinks) { - exemplars?.fields.forEach((field, index) => { - exemplars.fields[index].getLinks = getFieldLinks(exemplars, field); - }); - } + exemplars?.fields.forEach((field) => { + field.getLinks = getLinksSupplier(exemplars, field, field.state?.scopedVars ?? {}, replaceVariables); + }); if (options.calculate) { if (config.featureToggles.transformationsVariableSupport) { @@ -138,7 +136,7 @@ export function prepareHeatmapData( } // Everything past here assumes a field for each row in the heatmap (buckets) - if (!rowsHeatmap) { + if (rowsHeatmap == null) { if (frames.length > 1) { let allNamesNumeric = frames.every( (frame) => !Number.isNaN(parseSampleValue(frame.fields[1].state?.displayName!)) @@ -148,11 +146,10 @@ export function prepareHeatmapData( frames.sort(sortSeriesByLabel); } - rowsHeatmap = [ - outerJoinDataFrames({ - frames, - })!, - ][0]; + rowsHeatmap = outerJoinDataFrames({ + frames, + keepDisplayNames: true, + })!; } else { let frame = frames[0]; let numberFields = frame.fields.filter((field) => field.type === FieldType.number); @@ -171,18 +168,31 @@ export function prepareHeatmapData( } } - return getDenseHeatmapData( - rowsToCellsHeatmap({ - unit: options.yAxis?.unit, // used to format the ordinal lookup values - decimals: options.yAxis?.decimals, - ...options.rowsFrame, - frame: rowsHeatmap, - }), - exemplars, - options, - palette, - theme - ); + // config data links + rowsHeatmap.fields.forEach((field) => { + if ((field.config.links?.length ?? 0) === 0) { + return; + } + + // this expects that the tooltip is able to identify the field and rowIndex from a dense hovered index + field.getLinks = getLinksSupplier(rowsHeatmap!, field, field.state?.scopedVars ?? {}, replaceVariables); + }); + + return { + ...getDenseHeatmapData( + rowsToCellsHeatmap({ + unit: options.yAxis?.unit, // used to format the ordinal lookup values + decimals: options.yAxis?.decimals, + ...options.rowsFrame, + frame: rowsHeatmap, + }), + exemplars, + options, + palette, + theme + ), + series: rowsHeatmap, + }; } const getSparseHeatmapData = ( diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 58684eee516..5ac97b441c6 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -53,15 +53,7 @@ export const plugin = new PanelPlugin(HeatmapPanel) // NOTE: this feels like overkill/expensive just to assert if we have an ordinal y // can probably simplify without doing full dataprep const palette = quantizeScheme(opts.color, config.theme2); - const v = prepareHeatmapData( - context.data, - undefined, - opts, - palette, - config.theme2, - undefined, - context.replaceVariables - ); + const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2); isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; } catch {} } diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index 79747c7c5cb..141a94541d0 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -557,7 +557,8 @@ export function prepConfig(opts: PrepConfigOpts) { }); }, }, - exemplarFillColor + exemplarFillColor, + dataRef.current.yLayout ), theme, scaleKey: '', // facets' scales used (above) @@ -585,6 +586,10 @@ export function prepConfig(opts: PrepConfigOpts) { return hRect && seriesIdx === hRect.sidx ? hRect.didx : null; }, + focus: { + prox: 1e3, + dist: (u, seriesIdx) => (hRect?.sidx === seriesIdx ? 0 : Infinity), + }, points: { fill: 'rgba(255,255,255, 0.3)', bbox: (u, seriesIdx) => { @@ -744,7 +749,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) { }; } -export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: string) { +export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: string, yLayout?: HeatmapCellLayout) { return (u: uPlot, seriesIdx: number) => { uPlot.orient( u, @@ -772,6 +777,8 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin let fillPaths = [points]; let fillPalette = [exemplarColor ?? 'rgba(255,0,255,0.7)']; + let yShift = yLayout === HeatmapCellLayout.le ? -0.5 : yLayout === HeatmapCellLayout.ge ? 0.5 : 0; + for (let i = 0; i < dataX.length; i++) { let yVal = dataY[i]!; @@ -782,10 +789,7 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin let isSparseHeatmap = scaleY.distr === 3 && scaleY.log === 2; if (!isSparseHeatmap) { - yVal -= 0.5; // center vertically in bucket (when tiles are le) - // y-randomize vertically to distribute exemplars in same bucket at same time - let randSign = Math.round(Math.random()) * 2 - 1; - yVal += randSign * 0.5 * Math.random(); + yVal += yShift; } let x = valToPosX(dataX[i], scaleX, xDim, xOff); diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index d4484973ddd..997d7e43c21 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -56,7 +56,7 @@ export const TimeSeriesTooltip = ({ seriesIdx, mode, sortOrder, - (field) => field.type === FieldType.number + (field) => field.type === FieldType.number || field.type === FieldType.enum ); let footer: ReactNode;