mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
VizTooltips: Fix sorting (#82278)
This commit is contained in:
parent
80f324fadb
commit
4b67ac117f
@ -19,11 +19,14 @@ export enum ColorPlacement {
|
|||||||
|
|
||||||
export interface LabelValue {
|
export interface LabelValue {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number | null;
|
value: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
colorIndicator?: ColorIndicator;
|
colorIndicator?: ColorIndicator;
|
||||||
colorPlacement?: ColorPlacement;
|
colorPlacement?: ColorPlacement;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
|
||||||
|
// internal/tmp for sorting
|
||||||
|
numeric?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_COLOR_INDICATOR = ColorIndicator.series;
|
export const DEFAULT_COLOR_INDICATOR = ColorIndicator.series;
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { calculateTooltipPosition } from './utils';
|
import { DataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { calculateTooltipPosition, getContentItems } from './utils';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
describe('calculateTooltipPosition', () => {
|
describe('calculateTooltipPosition', () => {
|
||||||
@ -162,4 +165,171 @@ describe('utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('it tests getContentItems with numeric values', () => {
|
||||||
|
const timeValues = [1707833954056, 1707838274056, 1707842594056];
|
||||||
|
const seriesAValues = [1, 20, 70];
|
||||||
|
const seriesBValues = [-100, -26, null];
|
||||||
|
|
||||||
|
const frame = {
|
||||||
|
name: 'a',
|
||||||
|
length: timeValues.length,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: timeValues[0],
|
||||||
|
config: {},
|
||||||
|
display: (value: string) => ({
|
||||||
|
text: value,
|
||||||
|
color: undefined,
|
||||||
|
numeric: NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'A-series',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: seriesAValues,
|
||||||
|
config: {},
|
||||||
|
display: (value: string) => ({
|
||||||
|
text: value,
|
||||||
|
color: undefined,
|
||||||
|
numeric: Number(value),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'B-series',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: seriesBValues,
|
||||||
|
config: {},
|
||||||
|
display: (value: string) => ({
|
||||||
|
text: value,
|
||||||
|
color: undefined,
|
||||||
|
numeric: Number(value),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as DataFrame;
|
||||||
|
|
||||||
|
const fields = frame.fields;
|
||||||
|
const xField = frame.fields[0];
|
||||||
|
const dataIdxs = [1, 1, 1];
|
||||||
|
|
||||||
|
it('displays one series in single mode', () => {
|
||||||
|
const rows = getContentItems(fields, xField, dataIdxs, 2, TooltipDisplayMode.Single, SortOrder.None);
|
||||||
|
expect(rows.length).toBe(1);
|
||||||
|
expect(rows[0].value).toBe('-26');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the right content in multi mode', () => {
|
||||||
|
const rows = getContentItems(fields, xField, dataIdxs, null, TooltipDisplayMode.Multi, SortOrder.None);
|
||||||
|
expect(rows.length).toBe(2);
|
||||||
|
expect(rows[0].value).toBe('20');
|
||||||
|
expect(rows[1].value).toBe('-26');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the values sorted ASC', () => {
|
||||||
|
const rows = getContentItems(fields, xField, dataIdxs, null, TooltipDisplayMode.Multi, SortOrder.Ascending);
|
||||||
|
expect(rows.length).toBe(2);
|
||||||
|
expect(rows[0].value).toBe('-26');
|
||||||
|
expect(rows[1].value).toBe('20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the values sorted DESC', () => {
|
||||||
|
const rows = getContentItems(fields, xField, dataIdxs, null, TooltipDisplayMode.Multi, SortOrder.Descending);
|
||||||
|
expect(rows.length).toBe(2);
|
||||||
|
expect(rows[0].value).toBe('20');
|
||||||
|
expect(rows[1].value).toBe('-26');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the correct value when NULL values', () => {
|
||||||
|
const rows = getContentItems(fields, xField, [2, 2, null], null, TooltipDisplayMode.Multi, SortOrder.Descending);
|
||||||
|
expect(rows.length).toBe(1);
|
||||||
|
expect(rows[0].value).toBe('70');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('it tests getContentItems with string values', () => {
|
||||||
|
const timeValues = [1707833954056, 1707838274056, 1707842594056];
|
||||||
|
const seriesAValues = ['LOW', 'HIGH', 'NORMAL'];
|
||||||
|
const seriesBValues = ['NORMAL', 'LOW', 'LOW'];
|
||||||
|
|
||||||
|
const frame = {
|
||||||
|
name: 'a',
|
||||||
|
length: timeValues.length,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: timeValues[0],
|
||||||
|
config: {},
|
||||||
|
display: (value: string) => ({
|
||||||
|
text: value,
|
||||||
|
color: undefined,
|
||||||
|
numeric: NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'A-series',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: seriesAValues,
|
||||||
|
config: {},
|
||||||
|
display: (value: string) => ({
|
||||||
|
text: value,
|
||||||
|
color: undefined,
|
||||||
|
numeric: NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'B-series',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: seriesBValues,
|
||||||
|
config: {},
|
||||||
|
display: (value: string) => ({
|
||||||
|
text: value,
|
||||||
|
color: undefined,
|
||||||
|
numeric: NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as DataFrame;
|
||||||
|
|
||||||
|
const fields = frame.fields;
|
||||||
|
const xField = frame.fields[0];
|
||||||
|
const dataIdxs = [null, 0, 0];
|
||||||
|
|
||||||
|
it('displays one series in single mode', () => {
|
||||||
|
const rows = getContentItems(fields, xField, [null, null, 0], 2, TooltipDisplayMode.Single, SortOrder.None);
|
||||||
|
expect(rows.length).toBe(1);
|
||||||
|
expect(rows[0].value).toBe('NORMAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the right content in multi mode', () => {
|
||||||
|
const rows = getContentItems(fields, xField, dataIdxs, 2, TooltipDisplayMode.Multi, SortOrder.None);
|
||||||
|
expect(rows.length).toBe(2);
|
||||||
|
expect(rows[0].value).toBe('LOW');
|
||||||
|
expect(rows[1].value).toBe('NORMAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the values sorted ASC', () => {
|
||||||
|
const rows = getContentItems(fields, xField, dataIdxs, 2, TooltipDisplayMode.Multi, SortOrder.Ascending);
|
||||||
|
expect(rows.length).toBe(2);
|
||||||
|
expect(rows[0].value).toBe('LOW');
|
||||||
|
expect(rows[1].value).toBe('NORMAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the values sorted DESC', () => {
|
||||||
|
const rows = getContentItems(
|
||||||
|
frame.fields,
|
||||||
|
frame.fields[0],
|
||||||
|
dataIdxs,
|
||||||
|
2,
|
||||||
|
TooltipDisplayMode.Multi,
|
||||||
|
SortOrder.Descending
|
||||||
|
);
|
||||||
|
expect(rows.length).toBe(2);
|
||||||
|
expect(rows[0].value).toBe('NORMAL');
|
||||||
|
expect(rows[1].value).toBe('LOW');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import { FALLBACK_COLOR, Field, FieldType, formattedValueToString } from '@grafana/data';
|
||||||
|
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
|
||||||
|
|
||||||
import { ColorIndicatorStyles } from './VizTooltipColorIndicator';
|
import { ColorIndicatorStyles } from './VizTooltipColorIndicator';
|
||||||
import { ColorIndicator } from './types';
|
import { ColorIndicator, ColorPlacement, LabelValue } from './types';
|
||||||
|
|
||||||
export const calculateTooltipPosition = (
|
export const calculateTooltipPosition = (
|
||||||
xPos = 0,
|
xPos = 0,
|
||||||
@ -66,3 +69,80 @@ export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndi
|
|||||||
return styles.value;
|
return styles.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const numberCmp = (a: LabelValue, b: LabelValue) => a.numeric! - b.numeric!;
|
||||||
|
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
const stringCmp = (a: LabelValue, b: LabelValue) => collator.compare(`${a.value}`, `${b.value}`);
|
||||||
|
|
||||||
|
export const getContentItems = (
|
||||||
|
fields: Field[],
|
||||||
|
xField: Field,
|
||||||
|
dataIdxs: Array<number | null>,
|
||||||
|
seriesIdx: number | null | undefined,
|
||||||
|
mode: TooltipDisplayMode,
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
fieldFilter = (field: Field) => true
|
||||||
|
): LabelValue[] => {
|
||||||
|
let rows: LabelValue[] = [];
|
||||||
|
|
||||||
|
let allNumeric = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
const field = fields[i];
|
||||||
|
|
||||||
|
if (
|
||||||
|
field === xField ||
|
||||||
|
field.type === FieldType.time ||
|
||||||
|
!fieldFilter(field) ||
|
||||||
|
field.config.custom?.hideFrom?.tooltip ||
|
||||||
|
field.config.custom?.hideFrom?.viz
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in single mode, skip all but closest field
|
||||||
|
if (mode === TooltipDisplayMode.Single && seriesIdx !== i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataIdx = dataIdxs[i];
|
||||||
|
|
||||||
|
// omit non-hovered
|
||||||
|
if (dataIdx == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(field.type === FieldType.number || field.type === FieldType.boolean || field.type === FieldType.enum)) {
|
||||||
|
allNumeric = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = fields[i].values[dataIdx];
|
||||||
|
|
||||||
|
// no value -> zero?
|
||||||
|
const display = field.display!(v); // super expensive :(
|
||||||
|
// sort NaN and non-numeric to bottom (regardless of sort order)
|
||||||
|
const numeric = !Number.isNaN(display.numeric)
|
||||||
|
? display.numeric
|
||||||
|
: sortOrder === SortOrder.Descending
|
||||||
|
? Number.MIN_SAFE_INTEGER
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
label: field.state?.displayName ?? field.name,
|
||||||
|
value: formattedValueToString(display),
|
||||||
|
color: display.color ?? FALLBACK_COLOR,
|
||||||
|
colorIndicator: ColorIndicator.series,
|
||||||
|
colorPlacement: ColorPlacement.first,
|
||||||
|
isActive: mode === TooltipDisplayMode.Multi && seriesIdx === i,
|
||||||
|
numeric,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder !== SortOrder.None && rows.length > 1) {
|
||||||
|
const cmp = allNumeric ? numberCmp : stringCmp;
|
||||||
|
const mult = sortOrder === SortOrder.Descending ? -1 : 1;
|
||||||
|
rows.sort((a, b) => mult * cmp(a, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data';
|
import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data';
|
||||||
import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema';
|
import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode } from '@grafana/schema';
|
||||||
import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui';
|
import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui';
|
||||||
|
|
||||||
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
||||||
@ -58,6 +58,8 @@ export class TimelineChart extends React.Component<TimelineProps> {
|
|||||||
// When there is only one row, use the full space
|
// When there is only one row, use the full space
|
||||||
rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1,
|
rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1,
|
||||||
getValueColor: this.getValueColor,
|
getValueColor: this.getValueColor,
|
||||||
|
// @ts-ignore
|
||||||
|
hoverMulti: this.props.tooltip.mode === TooltipDisplayMode.Multi,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ export interface TimelineCoreOptions {
|
|||||||
getFieldConfig: (seriesIdx: number) => StateTimeLineFieldConfig | StatusHistoryFieldConfig;
|
getFieldConfig: (seriesIdx: number) => StateTimeLineFieldConfig | StatusHistoryFieldConfig;
|
||||||
onHover: (seriesIdx: number, valueIdx: number, rect: Rect) => void;
|
onHover: (seriesIdx: number, valueIdx: number, rect: Rect) => void;
|
||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
|
hoverMulti: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,6 +80,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
|||||||
getFieldConfig,
|
getFieldConfig,
|
||||||
onHover,
|
onHover,
|
||||||
onLeave,
|
onLeave,
|
||||||
|
hoverMulti,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
let qt: Quadtree;
|
let qt: Quadtree;
|
||||||
@ -405,8 +407,6 @@ export function getConfig(opts: TimelineCoreOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoverMulti = mode === TimelineMode.Changes;
|
|
||||||
|
|
||||||
const cursor: uPlot.Cursor = {
|
const cursor: uPlot.Cursor = {
|
||||||
x: mode === TimelineMode.Changes,
|
x: mode === TimelineMode.Changes,
|
||||||
y: false,
|
y: false,
|
||||||
|
@ -63,6 +63,7 @@ interface UPlotConfigOptions {
|
|||||||
getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string;
|
getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string;
|
||||||
// Identifies the shared key for uPlot cursor sync
|
// Identifies the shared key for uPlot cursor sync
|
||||||
eventsScope?: string;
|
eventsScope?: string;
|
||||||
|
hoverMulti: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,6 +106,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
|||||||
mergeValues,
|
mergeValues,
|
||||||
getValueColor,
|
getValueColor,
|
||||||
eventsScope = '__global_',
|
eventsScope = '__global_',
|
||||||
|
hoverMulti,
|
||||||
}) => {
|
}) => {
|
||||||
const builder = new UPlotConfigBuilder(timeZones[0]);
|
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||||||
|
|
||||||
@ -165,6 +167,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
|||||||
hoveredDataIdx = null;
|
hoveredDataIdx = null;
|
||||||
shouldChangeHover = true;
|
shouldChangeHover = true;
|
||||||
},
|
},
|
||||||
|
hoverMulti,
|
||||||
};
|
};
|
||||||
|
|
||||||
let shouldChangeHover = false;
|
let shouldChangeHover = false;
|
||||||
|
@ -297,6 +297,7 @@ export const CandlestickPanel = ({
|
|||||||
dataIdxs={dataIdxs}
|
dataIdxs={dataIdxs}
|
||||||
seriesIdx={seriesIdx}
|
seriesIdx={seriesIdx}
|
||||||
mode={options.tooltip.mode}
|
mode={options.tooltip.mode}
|
||||||
|
sortOrder={options.tooltip.sort}
|
||||||
isPinned={isPinned}
|
isPinned={isPinned}
|
||||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||||
scrollable={isTooltipScrollable(options.tooltip)}
|
scrollable={isTooltipScrollable(options.tooltip)}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
@ -9,7 +8,6 @@ import {
|
|||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
getLinksSupplier,
|
getLinksSupplier,
|
||||||
GrafanaTheme2,
|
|
||||||
InterpolateFunction,
|
InterpolateFunction,
|
||||||
LinkModel,
|
LinkModel,
|
||||||
PanelData,
|
PanelData,
|
||||||
@ -26,6 +24,8 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
|||||||
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
||||||
|
|
||||||
|
import { getStyles } from '../timeseries/TimeSeriesTooltip';
|
||||||
|
|
||||||
import { HeatmapData } from './fields';
|
import { HeatmapData } from './fields';
|
||||||
import { renderHistogram } from './renderHistogram';
|
import { renderHistogram } from './renderHistogram';
|
||||||
import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils';
|
import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils';
|
||||||
@ -112,7 +112,7 @@ const HeatmapHoverCell = ({
|
|||||||
|
|
||||||
let nonNumericOrdinalDisplay: string | undefined = undefined;
|
let nonNumericOrdinalDisplay: string | undefined = undefined;
|
||||||
|
|
||||||
let contentLabelValue: LabelValue[] = [];
|
let contentItems: LabelValue[] = [];
|
||||||
|
|
||||||
const getYValueIndex = (idx: number) => {
|
const getYValueIndex = (idx: number) => {
|
||||||
return idx % data.yBucketCount! ?? 0;
|
return idx % data.yBucketCount! ?? 0;
|
||||||
@ -244,7 +244,7 @@ const HeatmapHoverCell = ({
|
|||||||
if (mode === TooltipDisplayMode.Single || isPinned) {
|
if (mode === TooltipDisplayMode.Single || isPinned) {
|
||||||
const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : [];
|
const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : [];
|
||||||
|
|
||||||
contentLabelValue = [
|
contentItems = [
|
||||||
{
|
{
|
||||||
label: getFieldDisplayName(countField, data.heatmap),
|
label: getFieldDisplayName(countField, data.heatmap),
|
||||||
value: data.display!(count),
|
value: data.display!(count),
|
||||||
@ -272,7 +272,7 @@ const HeatmapHoverCell = ({
|
|||||||
|
|
||||||
const vals: LabelValue[] = getDisplayData(fromIdx, toIdx);
|
const vals: LabelValue[] = getDisplayData(fromIdx, toIdx);
|
||||||
vals.forEach((val) => {
|
vals.forEach((val) => {
|
||||||
contentLabelValue.push({
|
contentItems.push({
|
||||||
label: val.label,
|
label: val.label,
|
||||||
value: val.value,
|
value: val.value,
|
||||||
color: val.color ?? '#FFF',
|
color: val.color ?? '#FFF',
|
||||||
@ -330,26 +330,17 @@ const HeatmapHoverCell = ({
|
|||||||
[index]
|
[index]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHeaderLabel = (): LabelValue => {
|
const headerLabel: LabelValue = {
|
||||||
return {
|
label: '',
|
||||||
label: '',
|
value: xDisp(xBucketMax!)!,
|
||||||
value: xDisp(xBucketMax)!,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContentLabelValue = (): LabelValue[] => {
|
let customContent: ReactElement[] = [];
|
||||||
return contentLabelValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCustomContent = () => {
|
|
||||||
let content: ReactElement[] = [];
|
|
||||||
if (mode !== TooltipDisplayMode.Single) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (mode === TooltipDisplayMode.Single) {
|
||||||
// Histogram
|
// Histogram
|
||||||
if (showHistogram) {
|
if (showHistogram && !isSparse) {
|
||||||
content.push(
|
customContent.push(
|
||||||
<canvas
|
<canvas
|
||||||
width={histCanWidth}
|
width={histCanWidth}
|
||||||
height={histCanHeight}
|
height={histCanHeight}
|
||||||
@ -361,7 +352,7 @@ const HeatmapHoverCell = ({
|
|||||||
|
|
||||||
// Color scale
|
// Color scale
|
||||||
if (colorPalette && showColorScale) {
|
if (colorPalette && showColorScale) {
|
||||||
content.push(
|
customContent.push(
|
||||||
<ColorScale
|
<ColorScale
|
||||||
colorPalette={colorPalette}
|
colorPalette={colorPalette}
|
||||||
min={data.heatmapColors?.minValue!}
|
min={data.heatmapColors?.minValue!}
|
||||||
@ -371,28 +362,15 @@ const HeatmapHoverCell = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return content;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
<VizTooltipHeader headerLabel={headerLabel} isPinned={isPinned} />
|
||||||
<VizTooltipContent
|
<VizTooltipContent contentLabelValue={contentItems} customContent={customContent} isPinned={isPinned} />
|
||||||
contentLabelValue={getContentLabelValue()}
|
|
||||||
customContent={getCustomContent()}
|
|
||||||
isPinned={isPinned}
|
|
||||||
/>
|
|
||||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
wrapper: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
@ -411,7 +411,7 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
|
|||||||
name: 'Show histogram (Y axis)',
|
name: 'Show histogram (Y axis)',
|
||||||
defaultValue: defaultOptions.tooltip.yHistogram,
|
defaultValue: defaultOptions.tooltip.yHistogram,
|
||||||
category,
|
category,
|
||||||
showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None,
|
showIf: (opts) => opts.tooltip.mode === TooltipDisplayMode.Single,
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addBooleanSwitch({
|
builder.addBooleanSwitch({
|
||||||
@ -419,7 +419,7 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
|
|||||||
name: 'Show color scale',
|
name: 'Show color scale',
|
||||||
defaultValue: defaultOptions.tooltip.showColorScale,
|
defaultValue: defaultOptions.tooltip.showColorScale,
|
||||||
category,
|
category,
|
||||||
showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None && config.featureToggles.newVizTooltips,
|
showIf: (opts) => opts.tooltip.mode === TooltipDisplayMode.Single && config.featureToggles.newVizTooltips,
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addNumberInput({
|
builder.addNumberInput({
|
||||||
|
@ -224,16 +224,16 @@ export const StateTimelinePanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StateTimelineTooltip2
|
<StateTimelineTooltip2
|
||||||
data={frames ?? []}
|
frames={frames ?? []}
|
||||||
|
seriesFrame={alignedFrame}
|
||||||
dataIdxs={dataIdxs}
|
dataIdxs={dataIdxs}
|
||||||
alignedData={alignedFrame}
|
|
||||||
seriesIdx={seriesIdx}
|
seriesIdx={seriesIdx}
|
||||||
timeZone={timeZone}
|
|
||||||
mode={options.tooltip.mode}
|
mode={options.tooltip.mode}
|
||||||
sortOrder={options.tooltip.sort}
|
sortOrder={options.tooltip.sort}
|
||||||
isPinned={isPinned}
|
isPinned={isPinned}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||||
|
withDuration={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -1,102 +1,58 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import { Field, FieldType, getFieldDisplayName, LinkModel, TimeRange } from '@grafana/data';
|
||||||
arrayUtils,
|
|
||||||
DataFrame,
|
|
||||||
Field,
|
|
||||||
FieldType,
|
|
||||||
getDisplayProcessor,
|
|
||||||
getFieldDisplayName,
|
|
||||||
GrafanaTheme2,
|
|
||||||
LinkModel,
|
|
||||||
TimeRange,
|
|
||||||
TimeZone,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen';
|
import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen';
|
||||||
import { TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
|
import { TooltipDisplayMode, useStyles2 } from '@grafana/ui';
|
||||||
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||||
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
|
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
|
||||||
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
|
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
|
||||||
import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
||||||
|
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils';
|
||||||
import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils';
|
import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils';
|
||||||
|
|
||||||
import { getDataLinks } from '../status-history/utils';
|
import { getDataLinks } from '../status-history/utils';
|
||||||
|
import { TimeSeriesTooltipProps, getStyles } from '../timeseries/TimeSeriesTooltip';
|
||||||
|
|
||||||
interface StateTimelineTooltip2Props {
|
interface StateTimelineTooltip2Props extends TimeSeriesTooltipProps {
|
||||||
data: DataFrame[];
|
|
||||||
alignedData: DataFrame;
|
|
||||||
dataIdxs: Array<number | null>;
|
|
||||||
seriesIdx: number | null | undefined;
|
|
||||||
isPinned: boolean;
|
|
||||||
timeZone?: TimeZone;
|
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
mode?: TooltipDisplayMode;
|
withDuration: boolean;
|
||||||
sortOrder?: SortOrder;
|
|
||||||
annotate?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StateTimelineTooltip2 = ({
|
export const StateTimelineTooltip2 = ({
|
||||||
data,
|
frames,
|
||||||
alignedData,
|
seriesFrame,
|
||||||
dataIdxs,
|
dataIdxs,
|
||||||
seriesIdx,
|
seriesIdx,
|
||||||
timeZone,
|
|
||||||
timeRange,
|
|
||||||
mode = TooltipDisplayMode.Single,
|
mode = TooltipDisplayMode.Single,
|
||||||
sortOrder = SortOrder.None,
|
sortOrder = SortOrder.None,
|
||||||
|
scrollable = false,
|
||||||
isPinned,
|
isPinned,
|
||||||
annotate,
|
annotate,
|
||||||
|
timeRange,
|
||||||
|
withDuration,
|
||||||
}: StateTimelineTooltip2Props) => {
|
}: StateTimelineTooltip2Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const theme = useTheme2();
|
|
||||||
|
|
||||||
const datapointIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null);
|
const xField = seriesFrame.fields[0];
|
||||||
|
|
||||||
if (datapointIdx == null || seriesIdx == null) {
|
const dataIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueFieldsCount = data.reduce(
|
const xVal = xField.display!(xField.values[dataIdx!]).text;
|
||||||
(acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
mode = isPinned ? TooltipDisplayMode.Single : mode;
|
||||||
* There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first
|
|
||||||
* from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count.
|
|
||||||
* Render nothing in this case to prevent error.
|
|
||||||
* See https://github.com/grafana/support-escalations/issues/932
|
|
||||||
*/
|
|
||||||
if (alignedData.fields.length - 1 !== valueFieldsCount || !alignedData.fields[seriesIdx]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contentLabelValue: LabelValue[] = [];
|
const contentItems = getContentItems(seriesFrame.fields, xField, dataIdxs, seriesIdx, mode, sortOrder);
|
||||||
|
|
||||||
const xField = alignedData.fields[0];
|
// append duration in single mode
|
||||||
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme });
|
if (withDuration && mode === TooltipDisplayMode.Single) {
|
||||||
|
const field = seriesFrame.fields[seriesIdx!];
|
||||||
let links: Array<LinkModel<Field>> = [];
|
const nextStateIdx = findNextStateIndex(field, dataIdx!);
|
||||||
|
|
||||||
const from = xFieldFmt(xField.values[datapointIdx!]).text;
|
|
||||||
|
|
||||||
// Single mode
|
|
||||||
if (mode === TooltipDisplayMode.Single || isPinned) {
|
|
||||||
const field = alignedData.fields[seriesIdx!];
|
|
||||||
links = getDataLinks(field, datapointIdx);
|
|
||||||
|
|
||||||
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme });
|
|
||||||
const value = field.values[datapointIdx!];
|
|
||||||
const display = fieldFmt(value);
|
|
||||||
|
|
||||||
const nextStateIdx = findNextStateIndex(field, datapointIdx!);
|
|
||||||
let nextStateTs;
|
let nextStateTs;
|
||||||
if (nextStateIdx) {
|
if (nextStateIdx) {
|
||||||
nextStateTs = xField.values[nextStateIdx!];
|
nextStateTs = xField.values[nextStateIdx!];
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateTs = xField.values[datapointIdx!];
|
const stateTs = xField.values[dataIdx!];
|
||||||
let duration: string;
|
let duration: string;
|
||||||
|
|
||||||
if (nextStateTs) {
|
if (nextStateTs) {
|
||||||
@ -106,88 +62,29 @@ export const StateTimelineTooltip2 = ({
|
|||||||
duration = fmtDuration(to - stateTs);
|
duration = fmtDuration(to - stateTs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationEntry: LabelValue[] = duration ? [{ label: 'Duration', value: duration }] : [];
|
contentItems.push({ label: 'Duration', value: duration });
|
||||||
|
|
||||||
contentLabelValue = [
|
|
||||||
{
|
|
||||||
label: getFieldDisplayName(field),
|
|
||||||
value: display.text,
|
|
||||||
color: display.color,
|
|
||||||
colorIndicator: ColorIndicator.value,
|
|
||||||
colorPlacement: ColorPlacement.trailing,
|
|
||||||
},
|
|
||||||
...durationEntry,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === TooltipDisplayMode.Multi && !isPinned) {
|
let links: Array<LinkModel<Field>> = [];
|
||||||
const fields = alignedData.fields;
|
|
||||||
const sortIdx: unknown[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < fields.length; i++) {
|
if (seriesIdx != null) {
|
||||||
const field = fields[i];
|
const field = seriesFrame.fields[seriesIdx];
|
||||||
if (
|
const dataIdx = dataIdxs[seriesIdx]!;
|
||||||
!field ||
|
links = getDataLinks(field, dataIdx);
|
||||||
field === xField ||
|
|
||||||
field.type === FieldType.time ||
|
|
||||||
field.config.custom?.hideFrom?.tooltip ||
|
|
||||||
field.config.custom?.hideFrom?.viz
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme });
|
|
||||||
const v = field.values[dataIdxs[i]!];
|
|
||||||
const display = fieldFmt(v);
|
|
||||||
|
|
||||||
sortIdx.push(v);
|
|
||||||
contentLabelValue.push({
|
|
||||||
label: getFieldDisplayName(field),
|
|
||||||
value: display.text,
|
|
||||||
color: display.color,
|
|
||||||
colorIndicator: ColorIndicator.value,
|
|
||||||
colorPlacement: ColorPlacement.trailing,
|
|
||||||
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 => {
|
const headerItem: LabelValue = {
|
||||||
return {
|
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
||||||
label: '',
|
value: xVal,
|
||||||
value: from,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContentLabelValue = (): LabelValue[] => {
|
|
||||||
return contentLabelValue;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div>
|
||||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
<div className={styles.wrapper}>
|
||||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
|
<VizTooltipHeader headerLabel={headerItem} isPinned={isPinned} />
|
||||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
<VizTooltipContent contentLabelValue={contentItems} isPinned={isPinned} scrollable={scrollable} />
|
||||||
|
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
wrapper: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
@ -22,13 +22,13 @@ import {
|
|||||||
TimelineMode,
|
TimelineMode,
|
||||||
} from 'app/core/components/TimelineChart/utils';
|
} from 'app/core/components/TimelineChart/utils';
|
||||||
|
|
||||||
|
import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2';
|
||||||
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
|
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
|
||||||
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
||||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||||
import { getTimezones } from '../timeseries/utils';
|
import { getTimezones } from '../timeseries/utils';
|
||||||
|
|
||||||
import { StatusHistoryTooltip } from './StatusHistoryTooltip';
|
import { StatusHistoryTooltip } from './StatusHistoryTooltip';
|
||||||
import { StatusHistoryTooltip2 } from './StatusHistoryTooltip2';
|
|
||||||
import { Options } from './panelcfg.gen';
|
import { Options } from './panelcfg.gen';
|
||||||
|
|
||||||
const TOOLTIP_OFFSET = 10;
|
const TOOLTIP_OFFSET = 10;
|
||||||
@ -251,16 +251,17 @@ export const StatusHistoryPanel = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusHistoryTooltip2
|
<StateTimelineTooltip2
|
||||||
data={frames ?? []}
|
frames={frames ?? []}
|
||||||
|
seriesFrame={alignedFrame}
|
||||||
dataIdxs={dataIdxs}
|
dataIdxs={dataIdxs}
|
||||||
alignedData={alignedFrame}
|
|
||||||
seriesIdx={seriesIdx}
|
seriesIdx={seriesIdx}
|
||||||
timeZone={timeZone}
|
|
||||||
mode={options.tooltip.mode}
|
mode={options.tooltip.mode}
|
||||||
sortOrder={options.tooltip.sort}
|
sortOrder={options.tooltip.sort}
|
||||||
isPinned={isPinned}
|
isPinned={isPinned}
|
||||||
|
timeRange={timeRange}
|
||||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||||
|
withDuration={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -1,158 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DataFrame,
|
|
||||||
Field,
|
|
||||||
formattedValueToString,
|
|
||||||
getDisplayProcessor,
|
|
||||||
getFieldDisplayName,
|
|
||||||
GrafanaTheme2,
|
|
||||||
TimeZone,
|
|
||||||
LinkModel,
|
|
||||||
FieldType,
|
|
||||||
arrayUtils,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
|
|
||||||
import { useStyles2 } 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 { getDataLinks } from './utils';
|
|
||||||
|
|
||||||
interface StatusHistoryTooltipProps {
|
|
||||||
data: DataFrame[];
|
|
||||||
dataIdxs: Array<number | null>;
|
|
||||||
alignedData: DataFrame;
|
|
||||||
seriesIdx: number | null | undefined;
|
|
||||||
timeZone: TimeZone;
|
|
||||||
isPinned: boolean;
|
|
||||||
mode?: TooltipDisplayMode;
|
|
||||||
sortOrder?: SortOrder;
|
|
||||||
annotate?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(field: Field, val: number): string {
|
|
||||||
if (field.display) {
|
|
||||||
return formattedValueToString(field.display(val));
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${val}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StatusHistoryTooltip2 = ({
|
|
||||||
dataIdxs,
|
|
||||||
alignedData,
|
|
||||||
seriesIdx,
|
|
||||||
mode = TooltipDisplayMode.Single,
|
|
||||||
sortOrder = SortOrder.None,
|
|
||||||
isPinned,
|
|
||||||
annotate,
|
|
||||||
}: StatusHistoryTooltipProps) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
const datapointIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null);
|
|
||||||
|
|
||||||
if (datapointIdx == null || seriesIdx == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contentLabelValue: LabelValue[] = [];
|
|
||||||
|
|
||||||
const xField = alignedData.fields[0];
|
|
||||||
let links: Array<LinkModel<Field>> = [];
|
|
||||||
|
|
||||||
// Single mode
|
|
||||||
if (mode === TooltipDisplayMode.Single || isPinned) {
|
|
||||||
const field = alignedData.fields[seriesIdx!];
|
|
||||||
links = getDataLinks(field, datapointIdx);
|
|
||||||
|
|
||||||
const fieldFmt = field.display || getDisplayProcessor();
|
|
||||||
const value = field.values[datapointIdx!];
|
|
||||||
const display = fieldFmt(value);
|
|
||||||
|
|
||||||
contentLabelValue = [
|
|
||||||
{
|
|
||||||
label: getFieldDisplayName(field),
|
|
||||||
value: fmt(field, field.values[datapointIdx]),
|
|
||||||
color: display.color,
|
|
||||||
colorIndicator: ColorIndicator.value,
|
|
||||||
colorPlacement: ColorPlacement.trailing,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === TooltipDisplayMode.Multi && !isPinned) {
|
|
||||||
const frame = alignedData;
|
|
||||||
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.config.custom?.hideFrom?.tooltip ||
|
|
||||||
field.config.custom?.hideFrom?.viz
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldFmt = field.display || getDisplayProcessor();
|
|
||||||
const v = field.values[datapointIdx!];
|
|
||||||
const display = fieldFmt(v);
|
|
||||||
|
|
||||||
sortIdx.push(v);
|
|
||||||
contentLabelValue.push({
|
|
||||||
label: getFieldDisplayName(field),
|
|
||||||
value: fmt(field, field.values[datapointIdx]),
|
|
||||||
color: display.color,
|
|
||||||
colorIndicator: ColorIndicator.value,
|
|
||||||
colorPlacement: ColorPlacement.trailing,
|
|
||||||
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: fmt(xField, xField.values[datapointIdx]),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContentLabelValue = () => {
|
|
||||||
return contentLabelValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.wrapper}>
|
|
||||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
|
||||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
|
|
||||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
wrapper: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}),
|
|
||||||
});
|
|
@ -1,31 +1,21 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import { DataFrame, FieldType, LinkModel, Field, getFieldDisplayName } from '@grafana/data';
|
||||||
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 { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen';
|
||||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||||
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
|
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
|
||||||
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
|
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
|
||||||
import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
||||||
|
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils';
|
||||||
|
|
||||||
import { getDataLinks } from '../status-history/utils';
|
import { getDataLinks } from '../status-history/utils';
|
||||||
|
|
||||||
// exemplar / annotation / time region hovering?
|
// exemplar / annotation / time region hovering?
|
||||||
// add annotation UI / alert dismiss UI?
|
// add annotation UI / alert dismiss UI?
|
||||||
|
|
||||||
interface TimeSeriesTooltipProps {
|
export interface TimeSeriesTooltipProps {
|
||||||
frames?: DataFrame[];
|
frames?: DataFrame[];
|
||||||
// aligned series frame
|
// aligned series frame
|
||||||
seriesFrame: DataFrame;
|
seriesFrame: DataFrame;
|
||||||
@ -53,119 +43,47 @@ export const TimeSeriesTooltip = ({
|
|||||||
isPinned,
|
isPinned,
|
||||||
annotate,
|
annotate,
|
||||||
}: TimeSeriesTooltipProps) => {
|
}: TimeSeriesTooltipProps) => {
|
||||||
const theme = useTheme2();
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const xField = seriesFrame.fields[0];
|
const xField = seriesFrame.fields[0];
|
||||||
if (!xField) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, theme });
|
const xVal = xField.display!(xField.values[dataIdxs[0]!]).text;
|
||||||
let xVal = xFieldFmt(xField!.values[dataIdxs[0]!]).text;
|
|
||||||
|
const contentItems = getContentItems(
|
||||||
|
seriesFrame.fields,
|
||||||
|
xField,
|
||||||
|
dataIdxs,
|
||||||
|
seriesIdx,
|
||||||
|
mode,
|
||||||
|
sortOrder,
|
||||||
|
(field) => field.type === FieldType.number
|
||||||
|
);
|
||||||
|
|
||||||
let links: Array<LinkModel<Field>> = [];
|
let links: Array<LinkModel<Field>> = [];
|
||||||
let contentLabelValue: LabelValue[] = [];
|
|
||||||
|
|
||||||
// Single mode
|
|
||||||
if (mode === TooltipDisplayMode.Single) {
|
|
||||||
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]);
|
|
||||||
|
|
||||||
|
if (seriesIdx != null) {
|
||||||
|
const field = seriesFrame.fields[seriesIdx];
|
||||||
|
const dataIdx = dataIdxs[seriesIdx]!;
|
||||||
links = getDataLinks(field, 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) {
|
const headerItem: LabelValue = {
|
||||||
const fields = seriesFrame.fields;
|
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
||||||
const sortIdx: unknown[] = [];
|
value: xVal,
|
||||||
|
|
||||||
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]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seriesIdx != null) {
|
|
||||||
const field = seriesFrame.fields[seriesIdx];
|
|
||||||
const dataIdx = dataIdxs[seriesIdx]!;
|
|
||||||
links = getDataLinks(field, dataIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getHeaderLabel = (): LabelValue => {
|
|
||||||
return {
|
|
||||||
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
|
||||||
value: xVal,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContentLabelValue = () => {
|
|
||||||
return contentLabelValue;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
<VizTooltipHeader headerLabel={headerItem} isPinned={isPinned} />
|
||||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} scrollable={scrollable} />
|
<VizTooltipContent contentLabelValue={contentItems} isPinned={isPinned} scrollable={scrollable} />
|
||||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
export const getStyles = () => ({
|
||||||
wrapper: css({
|
wrapper: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataFrame, Field, getFieldDisplayName, GrafanaTheme2, LinkModel } from '@grafana/data';
|
import { DataFrame, Field, getFieldDisplayName, LinkModel } from '@grafana/data';
|
||||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||||
@ -10,8 +9,10 @@ import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizToolt
|
|||||||
import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
||||||
import { getTitleFromHref } from 'app/features/explore/utils/links';
|
import { getTitleFromHref } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
|
import { getStyles } from '../timeseries/TimeSeriesTooltip';
|
||||||
|
|
||||||
import { Options } from './panelcfg.gen';
|
import { Options } from './panelcfg.gen';
|
||||||
import { ScatterSeries, YValue } from './types';
|
import { ScatterSeries } from './types';
|
||||||
import { fmt } from './utils';
|
import { fmt } from './utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -41,66 +42,46 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss,
|
|||||||
const xField = series.x(frame);
|
const xField = series.x(frame);
|
||||||
const yField = series.y(frame);
|
const yField = series.y(frame);
|
||||||
|
|
||||||
const getHeaderLabel = (): LabelValue => {
|
let label = series.name;
|
||||||
let label = series.name;
|
if (options.seriesMapping === 'manual') {
|
||||||
if (options.seriesMapping === 'manual') {
|
label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`;
|
||||||
label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let colorThing = series.pointColor(frame);
|
let colorThing = series.pointColor(frame);
|
||||||
|
|
||||||
if (Array.isArray(colorThing)) {
|
if (Array.isArray(colorThing)) {
|
||||||
colorThing = colorThing[rowIndex];
|
colorThing = colorThing[rowIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const headerItem: LabelValue = {
|
||||||
label,
|
label,
|
||||||
value: null,
|
value: '',
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
color: alpha(colorThing as string, 0.5),
|
color: alpha(colorThing as string, 0.5),
|
||||||
colorIndicator: ColorIndicator.marker_md,
|
colorIndicator: ColorIndicator.marker_md,
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContentLabel = (): LabelValue[] => {
|
const contentItems: LabelValue[] = [
|
||||||
let colorThing = series.pointColor(frame);
|
{
|
||||||
|
label: getFieldDisplayName(xField, frame),
|
||||||
|
value: fmt(xField, xField.values[rowIndex]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: getFieldDisplayName(yField, frame),
|
||||||
|
value: fmt(yField, yField.values[rowIndex]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (Array.isArray(colorThing)) {
|
// add extra fields
|
||||||
colorThing = colorThing[rowIndex];
|
const extraFields: Field[] = frame.fields.filter((f) => f !== xField && f !== yField);
|
||||||
}
|
if (extraFields) {
|
||||||
|
extraFields.forEach((field) => {
|
||||||
const yValue: YValue = {
|
contentItems.push({
|
||||||
name: getFieldDisplayName(yField, frame),
|
label: field.name,
|
||||||
val: yField.values[rowIndex],
|
value: fmt(field, field.values[rowIndex]),
|
||||||
field: yField,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
color: alpha(colorThing as string, 0.5),
|
|
||||||
};
|
|
||||||
|
|
||||||
const content: LabelValue[] = [
|
|
||||||
{
|
|
||||||
label: getFieldDisplayName(xField, frame),
|
|
||||||
value: fmt(xField, xField.values[rowIndex]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: yValue.name,
|
|
||||||
value: fmt(yValue.field, yValue.val),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// add extra fields
|
|
||||||
const extraFields: Field[] = frame.fields.filter((f) => f !== xField && f !== yField);
|
|
||||||
if (extraFields) {
|
|
||||||
extraFields.forEach((field) => {
|
|
||||||
content.push({
|
|
||||||
label: field.name,
|
|
||||||
value: fmt(field, field.values[rowIndex]),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
return content;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLinks = (): Array<LinkModel<Field>> => {
|
const getLinks = (): Array<LinkModel<Field>> => {
|
||||||
let links: Array<LinkModel<Field>> = [];
|
let links: Array<LinkModel<Field>> = [];
|
||||||
@ -120,16 +101,9 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
<VizTooltipHeader headerLabel={headerItem} isPinned={isPinned} />
|
||||||
<VizTooltipContent contentLabelValue={getContentLabel()} isPinned={isPinned} />
|
<VizTooltipContent contentLabelValue={contentItems} isPinned={isPinned} />
|
||||||
{isPinned && <VizTooltipFooter dataLinks={getLinks()} />}
|
{isPinned && <VizTooltipFooter dataLinks={getLinks()} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
wrapper: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
@ -51,13 +51,6 @@ export interface ScatterSeries {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YValue {
|
|
||||||
name: string;
|
|
||||||
val: number;
|
|
||||||
field: Field;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExtraFacets {
|
export interface ExtraFacets {
|
||||||
colorFacetFieldName: string;
|
colorFacetFieldName: string;
|
||||||
sizeFacetFieldName: string;
|
sizeFacetFieldName: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user