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 {
|
||||
label: string;
|
||||
value: string | number | null;
|
||||
value: string;
|
||||
color?: string;
|
||||
colorIndicator?: ColorIndicator;
|
||||
colorPlacement?: ColorPlacement;
|
||||
isActive?: boolean;
|
||||
|
||||
// internal/tmp for sorting
|
||||
numeric?: number;
|
||||
}
|
||||
|
||||
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('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 { ColorIndicator } from './types';
|
||||
import { ColorIndicator, ColorPlacement, LabelValue } from './types';
|
||||
|
||||
export const calculateTooltipPosition = (
|
||||
xPos = 0,
|
||||
@ -66,3 +69,80 @@ export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndi
|
||||
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 { 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 { 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
|
||||
rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1,
|
||||
getValueColor: this.getValueColor,
|
||||
// @ts-ignore
|
||||
hoverMulti: this.props.tooltip.mode === TooltipDisplayMode.Multi,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -55,6 +55,7 @@ export interface TimelineCoreOptions {
|
||||
getFieldConfig: (seriesIdx: number) => StateTimeLineFieldConfig | StatusHistoryFieldConfig;
|
||||
onHover: (seriesIdx: number, valueIdx: number, rect: Rect) => void;
|
||||
onLeave: () => void;
|
||||
hoverMulti: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,6 +80,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
getFieldConfig,
|
||||
onHover,
|
||||
onLeave,
|
||||
hoverMulti,
|
||||
} = opts;
|
||||
|
||||
let qt: Quadtree;
|
||||
@ -405,8 +407,6 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
const hoverMulti = mode === TimelineMode.Changes;
|
||||
|
||||
const cursor: uPlot.Cursor = {
|
||||
x: mode === TimelineMode.Changes,
|
||||
y: false,
|
||||
|
@ -63,6 +63,7 @@ interface UPlotConfigOptions {
|
||||
getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string;
|
||||
// Identifies the shared key for uPlot cursor sync
|
||||
eventsScope?: string;
|
||||
hoverMulti: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,6 +106,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
mergeValues,
|
||||
getValueColor,
|
||||
eventsScope = '__global_',
|
||||
hoverMulti,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||||
|
||||
@ -165,6 +167,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
hoveredDataIdx = null;
|
||||
shouldChangeHover = true;
|
||||
},
|
||||
hoverMulti,
|
||||
};
|
||||
|
||||
let shouldChangeHover = false;
|
||||
|
@ -297,6 +297,7 @@ export const CandlestickPanel = ({
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||
scrollable={isTooltipScrollable(options.tooltip)}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
@ -9,7 +8,6 @@ import {
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
getLinksSupplier,
|
||||
GrafanaTheme2,
|
||||
InterpolateFunction,
|
||||
LinkModel,
|
||||
PanelData,
|
||||
@ -26,6 +24,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 { getStyles } from '../timeseries/TimeSeriesTooltip';
|
||||
|
||||
import { HeatmapData } from './fields';
|
||||
import { renderHistogram } from './renderHistogram';
|
||||
import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils';
|
||||
@ -112,7 +112,7 @@ const HeatmapHoverCell = ({
|
||||
|
||||
let nonNumericOrdinalDisplay: string | undefined = undefined;
|
||||
|
||||
let contentLabelValue: LabelValue[] = [];
|
||||
let contentItems: LabelValue[] = [];
|
||||
|
||||
const getYValueIndex = (idx: number) => {
|
||||
return idx % data.yBucketCount! ?? 0;
|
||||
@ -244,7 +244,7 @@ const HeatmapHoverCell = ({
|
||||
if (mode === TooltipDisplayMode.Single || isPinned) {
|
||||
const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : [];
|
||||
|
||||
contentLabelValue = [
|
||||
contentItems = [
|
||||
{
|
||||
label: getFieldDisplayName(countField, data.heatmap),
|
||||
value: data.display!(count),
|
||||
@ -272,7 +272,7 @@ const HeatmapHoverCell = ({
|
||||
|
||||
const vals: LabelValue[] = getDisplayData(fromIdx, toIdx);
|
||||
vals.forEach((val) => {
|
||||
contentLabelValue.push({
|
||||
contentItems.push({
|
||||
label: val.label,
|
||||
value: val.value,
|
||||
color: val.color ?? '#FFF',
|
||||
@ -330,26 +330,17 @@ const HeatmapHoverCell = ({
|
||||
[index]
|
||||
);
|
||||
|
||||
const getHeaderLabel = (): LabelValue => {
|
||||
return {
|
||||
label: '',
|
||||
value: xDisp(xBucketMax)!,
|
||||
};
|
||||
const headerLabel: LabelValue = {
|
||||
label: '',
|
||||
value: xDisp(xBucketMax!)!,
|
||||
};
|
||||
|
||||
const getContentLabelValue = (): LabelValue[] => {
|
||||
return contentLabelValue;
|
||||
};
|
||||
|
||||
const getCustomContent = () => {
|
||||
let content: ReactElement[] = [];
|
||||
if (mode !== TooltipDisplayMode.Single) {
|
||||
return content;
|
||||
}
|
||||
let customContent: ReactElement[] = [];
|
||||
|
||||
if (mode === TooltipDisplayMode.Single) {
|
||||
// Histogram
|
||||
if (showHistogram) {
|
||||
content.push(
|
||||
if (showHistogram && !isSparse) {
|
||||
customContent.push(
|
||||
<canvas
|
||||
width={histCanWidth}
|
||||
height={histCanHeight}
|
||||
@ -361,7 +352,7 @@ const HeatmapHoverCell = ({
|
||||
|
||||
// Color scale
|
||||
if (colorPalette && showColorScale) {
|
||||
content.push(
|
||||
customContent.push(
|
||||
<ColorScale
|
||||
colorPalette={colorPalette}
|
||||
min={data.heatmapColors?.minValue!}
|
||||
@ -371,28 +362,15 @@ const HeatmapHoverCell = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
}
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
||||
<VizTooltipContent
|
||||
contentLabelValue={getContentLabelValue()}
|
||||
customContent={getCustomContent()}
|
||||
isPinned={isPinned}
|
||||
/>
|
||||
<VizTooltipHeader headerLabel={headerLabel} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={contentItems} customContent={customContent} isPinned={isPinned} />
|
||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||
</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)',
|
||||
defaultValue: defaultOptions.tooltip.yHistogram,
|
||||
category,
|
||||
showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None,
|
||||
showIf: (opts) => opts.tooltip.mode === TooltipDisplayMode.Single,
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
@ -419,7 +419,7 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
|
||||
name: 'Show color scale',
|
||||
defaultValue: defaultOptions.tooltip.showColorScale,
|
||||
category,
|
||||
showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None && config.featureToggles.newVizTooltips,
|
||||
showIf: (opts) => opts.tooltip.mode === TooltipDisplayMode.Single && config.featureToggles.newVizTooltips,
|
||||
});
|
||||
|
||||
builder.addNumberInput({
|
||||
|
@ -224,16 +224,16 @@ export const StateTimelinePanel = ({
|
||||
|
||||
return (
|
||||
<StateTimelineTooltip2
|
||||
data={frames ?? []}
|
||||
frames={frames ?? []}
|
||||
seriesFrame={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
alignedData={alignedFrame}
|
||||
seriesIdx={seriesIdx}
|
||||
timeZone={timeZone}
|
||||
mode={options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
timeRange={timeRange}
|
||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||
withDuration={true}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -1,102 +1,58 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
arrayUtils,
|
||||
DataFrame,
|
||||
Field,
|
||||
FieldType,
|
||||
getDisplayProcessor,
|
||||
getFieldDisplayName,
|
||||
GrafanaTheme2,
|
||||
LinkModel,
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { Field, FieldType, getFieldDisplayName, LinkModel, TimeRange } from '@grafana/data';
|
||||
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 { 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 { 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 { getDataLinks } from '../status-history/utils';
|
||||
import { TimeSeriesTooltipProps, getStyles } from '../timeseries/TimeSeriesTooltip';
|
||||
|
||||
interface StateTimelineTooltip2Props {
|
||||
data: DataFrame[];
|
||||
alignedData: DataFrame;
|
||||
dataIdxs: Array<number | null>;
|
||||
seriesIdx: number | null | undefined;
|
||||
isPinned: boolean;
|
||||
timeZone?: TimeZone;
|
||||
interface StateTimelineTooltip2Props extends TimeSeriesTooltipProps {
|
||||
timeRange: TimeRange;
|
||||
mode?: TooltipDisplayMode;
|
||||
sortOrder?: SortOrder;
|
||||
annotate?: () => void;
|
||||
withDuration: boolean;
|
||||
}
|
||||
|
||||
export const StateTimelineTooltip2 = ({
|
||||
data,
|
||||
alignedData,
|
||||
frames,
|
||||
seriesFrame,
|
||||
dataIdxs,
|
||||
seriesIdx,
|
||||
timeZone,
|
||||
timeRange,
|
||||
mode = TooltipDisplayMode.Single,
|
||||
sortOrder = SortOrder.None,
|
||||
scrollable = false,
|
||||
isPinned,
|
||||
annotate,
|
||||
timeRange,
|
||||
withDuration,
|
||||
}: StateTimelineTooltip2Props) => {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
const dataIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null);
|
||||
|
||||
const valueFieldsCount = data.reduce(
|
||||
(acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length,
|
||||
0
|
||||
);
|
||||
const xVal = xField.display!(xField.values[dataIdx!]).text;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
mode = isPinned ? TooltipDisplayMode.Single : mode;
|
||||
|
||||
let contentLabelValue: LabelValue[] = [];
|
||||
const contentItems = getContentItems(seriesFrame.fields, xField, dataIdxs, seriesIdx, mode, sortOrder);
|
||||
|
||||
const xField = alignedData.fields[0];
|
||||
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme });
|
||||
|
||||
let links: Array<LinkModel<Field>> = [];
|
||||
|
||||
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!);
|
||||
// append duration in single mode
|
||||
if (withDuration && mode === TooltipDisplayMode.Single) {
|
||||
const field = seriesFrame.fields[seriesIdx!];
|
||||
const nextStateIdx = findNextStateIndex(field, dataIdx!);
|
||||
let nextStateTs;
|
||||
if (nextStateIdx) {
|
||||
nextStateTs = xField.values[nextStateIdx!];
|
||||
}
|
||||
|
||||
const stateTs = xField.values[datapointIdx!];
|
||||
const stateTs = xField.values[dataIdx!];
|
||||
let duration: string;
|
||||
|
||||
if (nextStateTs) {
|
||||
@ -106,88 +62,29 @@ export const StateTimelineTooltip2 = ({
|
||||
duration = fmtDuration(to - stateTs);
|
||||
}
|
||||
|
||||
const durationEntry: LabelValue[] = duration ? [{ label: 'Duration', value: duration }] : [];
|
||||
|
||||
contentLabelValue = [
|
||||
{
|
||||
label: getFieldDisplayName(field),
|
||||
value: display.text,
|
||||
color: display.color,
|
||||
colorIndicator: ColorIndicator.value,
|
||||
colorPlacement: ColorPlacement.trailing,
|
||||
},
|
||||
...durationEntry,
|
||||
];
|
||||
contentItems.push({ label: 'Duration', value: duration });
|
||||
}
|
||||
|
||||
if (mode === TooltipDisplayMode.Multi && !isPinned) {
|
||||
const fields = alignedData.fields;
|
||||
const sortIdx: unknown[] = [];
|
||||
let links: Array<LinkModel<Field>> = [];
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = 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({ 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]);
|
||||
});
|
||||
}
|
||||
if (seriesIdx != null) {
|
||||
const field = seriesFrame.fields[seriesIdx];
|
||||
const dataIdx = dataIdxs[seriesIdx]!;
|
||||
links = getDataLinks(field, dataIdx);
|
||||
}
|
||||
|
||||
const getHeaderLabel = (): LabelValue => {
|
||||
return {
|
||||
label: '',
|
||||
value: from,
|
||||
};
|
||||
};
|
||||
|
||||
const getContentLabelValue = (): LabelValue[] => {
|
||||
return contentLabelValue;
|
||||
const headerItem: LabelValue = {
|
||||
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
||||
value: xVal,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} />
|
||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||
<div>
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader headerLabel={headerItem} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={contentItems} isPinned={isPinned} scrollable={scrollable} />
|
||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
});
|
||||
|
@ -22,13 +22,13 @@ import {
|
||||
TimelineMode,
|
||||
} from 'app/core/components/TimelineChart/utils';
|
||||
|
||||
import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2';
|
||||
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
|
||||
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||
import { getTimezones } from '../timeseries/utils';
|
||||
|
||||
import { StatusHistoryTooltip } from './StatusHistoryTooltip';
|
||||
import { StatusHistoryTooltip2 } from './StatusHistoryTooltip2';
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
||||
const TOOLTIP_OFFSET = 10;
|
||||
@ -251,16 +251,17 @@ export const StatusHistoryPanel = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StatusHistoryTooltip2
|
||||
data={frames ?? []}
|
||||
<StateTimelineTooltip2
|
||||
frames={frames ?? []}
|
||||
seriesFrame={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
alignedData={alignedFrame}
|
||||
seriesIdx={seriesIdx}
|
||||
timeZone={timeZone}
|
||||
mode={options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
timeRange={timeRange}
|
||||
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 React from 'react';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
FALLBACK_COLOR,
|
||||
FieldType,
|
||||
GrafanaTheme2,
|
||||
formattedValueToString,
|
||||
getDisplayProcessor,
|
||||
LinkModel,
|
||||
Field,
|
||||
getFieldDisplayName,
|
||||
arrayUtils,
|
||||
} from '@grafana/data';
|
||||
import { DataFrame, FieldType, LinkModel, Field, getFieldDisplayName } from '@grafana/data';
|
||||
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 { 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 { LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
||||
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils';
|
||||
|
||||
import { getDataLinks } from '../status-history/utils';
|
||||
|
||||
// exemplar / annotation / time region hovering?
|
||||
// add annotation UI / alert dismiss UI?
|
||||
|
||||
interface TimeSeriesTooltipProps {
|
||||
export interface TimeSeriesTooltipProps {
|
||||
frames?: DataFrame[];
|
||||
// aligned series frame
|
||||
seriesFrame: DataFrame;
|
||||
@ -53,119 +43,47 @@ export const TimeSeriesTooltip = ({
|
||||
isPinned,
|
||||
annotate,
|
||||
}: TimeSeriesTooltipProps) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const xField = seriesFrame.fields[0];
|
||||
if (!xField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, theme });
|
||||
let xVal = xFieldFmt(xField!.values[dataIdxs[0]!]).text;
|
||||
const xVal = xField.display!(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 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);
|
||||
|
||||
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 fields = seriesFrame.fields;
|
||||
const sortIdx: unknown[] = [];
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = seriesFrame.fields[i];
|
||||
if (
|
||||
!field ||
|
||||
field === xField ||
|
||||
field.type === FieldType.time ||
|
||||
field.type !== FieldType.number ||
|
||||
field.config.custom?.hideFrom?.tooltip ||
|
||||
field.config.custom?.hideFrom?.viz
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const v = seriesFrame.fields[i].values[dataIdxs[i]!];
|
||||
const display = field.display!(v); // super expensive :(
|
||||
|
||||
sortIdx.push(v);
|
||||
contentLabelValue.push({
|
||||
label: field.state?.displayName ?? field.name,
|
||||
value: display ? formattedValueToString(display) : null,
|
||||
color: display.color || FALLBACK_COLOR,
|
||||
colorIndicator: ColorIndicator.series,
|
||||
colorPlacement: ColorPlacement.first,
|
||||
isActive: seriesIdx === i,
|
||||
});
|
||||
|
||||
if (sortOrder !== SortOrder.None) {
|
||||
// create sort reference series array, as Array.sort() mutates the original array
|
||||
const sortRef = [...contentLabelValue];
|
||||
const sortFn = arrayUtils.sortValues(sortOrder);
|
||||
|
||||
contentLabelValue.sort((a, b) => {
|
||||
// get compared values indices to retrieve raw values from sortIdx
|
||||
const aIdx = sortRef.indexOf(a);
|
||||
const bIdx = sortRef.indexOf(b);
|
||||
return sortFn(sortIdx[aIdx], sortIdx[bIdx]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const headerItem: LabelValue = {
|
||||
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
||||
value: xVal,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} isPinned={isPinned} scrollable={scrollable} />
|
||||
<VizTooltipHeader headerLabel={headerItem} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={contentItems} isPinned={isPinned} scrollable={scrollable} />
|
||||
{isPinned && <VizTooltipFooter dataLinks={links} annotate={annotate} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
export const getStyles = () => ({
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
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 { useStyles2 } from '@grafana/ui';
|
||||
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 { getTitleFromHref } from 'app/features/explore/utils/links';
|
||||
|
||||
import { getStyles } from '../timeseries/TimeSeriesTooltip';
|
||||
|
||||
import { Options } from './panelcfg.gen';
|
||||
import { ScatterSeries, YValue } from './types';
|
||||
import { ScatterSeries } from './types';
|
||||
import { fmt } from './utils';
|
||||
|
||||
export interface Props {
|
||||
@ -41,66 +42,46 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss,
|
||||
const xField = series.x(frame);
|
||||
const yField = series.y(frame);
|
||||
|
||||
const getHeaderLabel = (): LabelValue => {
|
||||
let label = series.name;
|
||||
if (options.seriesMapping === 'manual') {
|
||||
label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`;
|
||||
}
|
||||
let label = series.name;
|
||||
if (options.seriesMapping === 'manual') {
|
||||
label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`;
|
||||
}
|
||||
|
||||
let colorThing = series.pointColor(frame);
|
||||
let colorThing = series.pointColor(frame);
|
||||
|
||||
if (Array.isArray(colorThing)) {
|
||||
colorThing = colorThing[rowIndex];
|
||||
}
|
||||
if (Array.isArray(colorThing)) {
|
||||
colorThing = colorThing[rowIndex];
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: null,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
color: alpha(colorThing as string, 0.5),
|
||||
colorIndicator: ColorIndicator.marker_md,
|
||||
};
|
||||
const headerItem: LabelValue = {
|
||||
label,
|
||||
value: '',
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
color: alpha(colorThing as string, 0.5),
|
||||
colorIndicator: ColorIndicator.marker_md,
|
||||
};
|
||||
|
||||
const getContentLabel = (): LabelValue[] => {
|
||||
let colorThing = series.pointColor(frame);
|
||||
const contentItems: LabelValue[] = [
|
||||
{
|
||||
label: getFieldDisplayName(xField, frame),
|
||||
value: fmt(xField, xField.values[rowIndex]),
|
||||
},
|
||||
{
|
||||
label: getFieldDisplayName(yField, frame),
|
||||
value: fmt(yField, yField.values[rowIndex]),
|
||||
},
|
||||
];
|
||||
|
||||
if (Array.isArray(colorThing)) {
|
||||
colorThing = colorThing[rowIndex];
|
||||
}
|
||||
|
||||
const yValue: YValue = {
|
||||
name: getFieldDisplayName(yField, frame),
|
||||
val: yField.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]),
|
||||
});
|
||||
// add extra fields
|
||||
const extraFields: Field[] = frame.fields.filter((f) => f !== xField && f !== yField);
|
||||
if (extraFields) {
|
||||
extraFields.forEach((field) => {
|
||||
contentItems.push({
|
||||
label: field.name,
|
||||
value: fmt(field, field.values[rowIndex]),
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const getLinks = (): Array<LinkModel<Field>> => {
|
||||
let links: Array<LinkModel<Field>> = [];
|
||||
@ -120,16 +101,9 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss,
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader headerLabel={getHeaderLabel()} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={getContentLabel()} isPinned={isPinned} />
|
||||
<VizTooltipHeader headerLabel={headerItem} isPinned={isPinned} />
|
||||
<VizTooltipContent contentLabelValue={contentItems} isPinned={isPinned} />
|
||||
{isPinned && <VizTooltipFooter dataLinks={getLinks()} />}
|
||||
</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 {
|
||||
colorFacetFieldName: string;
|
||||
sizeFacetFieldName: string;
|
||||
|
Loading…
Reference in New Issue
Block a user