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;