mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Heatmap: Update tooltip UX (#79429)
This commit is contained in:
@@ -10,7 +10,7 @@ import { LabelValue } from './types';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contentLabelValue: LabelValue[];
|
contentLabelValue: LabelValue[];
|
||||||
customContent?: ReactElement | null;
|
customContent?: ReactElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) => {
|
export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) => {
|
||||||
@@ -35,7 +35,13 @@ export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) =
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{customContent && <div className={styles.customContentPadding}>{customContent}</div>}
|
{customContent?.map((content, i) => {
|
||||||
|
return (
|
||||||
|
<div key={i} className={styles.customContentPadding}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
||||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
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';
|
||||||
@@ -28,7 +28,7 @@ import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverVi
|
|||||||
|
|
||||||
import { HeatmapData } from './fields';
|
import { HeatmapData } from './fields';
|
||||||
import { renderHistogram } from './renderHistogram';
|
import { renderHistogram } from './renderHistogram';
|
||||||
import { getSparseCellMinMax, formatMilliseconds, getFieldFromData, getHoverCellColor } from './tooltip/utils';
|
import { getSparseCellMinMax, getFieldFromData, getHoverCellColor, formatMilliseconds } from './tooltip/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dataIdxs: Array<number | null>;
|
dataIdxs: Array<number | null>;
|
||||||
@@ -213,37 +213,64 @@ const HeatmapHoverCell = ({
|
|||||||
|
|
||||||
const { cellColor, colorPalette } = getHoverCellColor(data, index);
|
const { cellColor, colorPalette } = getHoverCellColor(data, index);
|
||||||
|
|
||||||
const getLabelValue = (): LabelValue[] => {
|
const getContentLabels = (): LabelValue[] => {
|
||||||
|
if (nonNumericOrdinalDisplay) {
|
||||||
|
return [{ label: 'Name', value: nonNumericOrdinalDisplay }];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (data.yLayout) {
|
||||||
|
case HeatmapCellLayout.unknown:
|
||||||
|
return [{ label: '', value: yDisp(yBucketMin) }];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: getFieldDisplayName(countField, data.heatmap),
|
label: 'Bucket',
|
||||||
value: data.display!(count),
|
value: `${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`,
|
||||||
color: cellColor ?? '#FFF',
|
|
||||||
colorIndicator: ColorIndicator.value,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHeaderLabel = (): LabelValue => {
|
const getHeaderLabel = (): LabelValue => {
|
||||||
if (nonNumericOrdinalDisplay) {
|
|
||||||
return { label: 'Name', value: nonNumericOrdinalDisplay };
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data.yLayout) {
|
|
||||||
case HeatmapCellLayout.unknown:
|
|
||||||
return { label: '', value: yDisp(yBucketMin) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: 'Bucket',
|
label: '',
|
||||||
value: `${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`,
|
value: xDisp(xBucketMax)!,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Color scale
|
const getContentLabelValue = (): LabelValue[] => {
|
||||||
const getCustomValueDisplay = (): ReactElement | null => {
|
const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: getFieldDisplayName(countField, data.heatmap),
|
||||||
|
value: data.display!(count),
|
||||||
|
color: cellColor ?? '#FFF',
|
||||||
|
colorPlacement: ColorPlacement.trailing,
|
||||||
|
colorIndicator: ColorIndicator.value,
|
||||||
|
},
|
||||||
|
...getContentLabels(),
|
||||||
|
...fromToInt,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCustomContent = () => {
|
||||||
|
let content: ReactElement[] = [];
|
||||||
|
// Histogram
|
||||||
|
if (showHistogram) {
|
||||||
|
content.push(
|
||||||
|
<canvas
|
||||||
|
width={histCanWidth}
|
||||||
|
height={histCanHeight}
|
||||||
|
ref={can}
|
||||||
|
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color scale
|
||||||
if (colorPalette && showColorScale) {
|
if (colorPalette && showColorScale) {
|
||||||
return (
|
content.push(
|
||||||
<ColorScale
|
<ColorScale
|
||||||
colorPalette={colorPalette}
|
colorPalette={colorPalette}
|
||||||
min={data.heatmapColors?.minValue!}
|
min={data.heatmapColors?.minValue!}
|
||||||
@@ -254,42 +281,7 @@ const HeatmapHoverCell = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return content;
|
||||||
};
|
|
||||||
|
|
||||||
const getContentLabelValue = (): LabelValue[] => {
|
|
||||||
let fromToInt = [
|
|
||||||
{
|
|
||||||
label: 'From',
|
|
||||||
value: xDisp(xBucketMin)!,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (data.xLayout !== HeatmapCellLayout.unknown) {
|
|
||||||
fromToInt.push({ label: 'To', value: xDisp(xBucketMax)! });
|
|
||||||
|
|
||||||
if (interval) {
|
|
||||||
const formattedString = formatMilliseconds(interval);
|
|
||||||
fromToInt.push({ label: 'Interval', value: formattedString });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fromToInt;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCustomContent = (): ReactElement | null => {
|
|
||||||
if (showHistogram) {
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
width={histCanWidth}
|
|
||||||
height={histCanHeight}
|
|
||||||
ref={can}
|
|
||||||
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// @TODO remove this when adding annotations support
|
// @TODO remove this when adding annotations support
|
||||||
@@ -299,11 +291,7 @@ const HeatmapHoverCell = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<VizTooltipHeader
|
<VizTooltipHeader headerLabel={getHeaderLabel()} />
|
||||||
headerLabel={getHeaderLabel()}
|
|
||||||
keyValuePairs={getLabelValue()}
|
|
||||||
customValueDisplay={getCustomValueDisplay()}
|
|
||||||
/>
|
|
||||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} customContent={getCustomContent()} />
|
<VizTooltipContent contentLabelValue={getContentLabelValue()} customContent={getCustomContent()} />
|
||||||
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={canAnnotate} />}
|
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={canAnnotate} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ describe('heatmap tooltip utils', () => {
|
|||||||
it('converts ms to appropriate unit', async () => {
|
it('converts ms to appropriate unit', async () => {
|
||||||
let msToFormat = 10;
|
let msToFormat = 10;
|
||||||
let formatted = formatMilliseconds(msToFormat);
|
let formatted = formatMilliseconds(msToFormat);
|
||||||
expect(formatted).toBe('10 milliseconds');
|
expect(formatted).toBe('10 ms');
|
||||||
|
|
||||||
msToFormat = 1000;
|
msToFormat = 1000;
|
||||||
formatted = formatMilliseconds(msToFormat);
|
formatted = formatMilliseconds(msToFormat);
|
||||||
expect(formatted).toBe('1 second');
|
expect(formatted).toBe('1 s');
|
||||||
|
|
||||||
msToFormat = 1000 * 120;
|
msToFormat = 1000 * 120;
|
||||||
formatted = formatMilliseconds(msToFormat);
|
formatted = formatMilliseconds(msToFormat);
|
||||||
expect(formatted).toBe('2 minutes');
|
expect(formatted).toBe('2 m');
|
||||||
|
|
||||||
msToFormat = 1000 * 60 * 60;
|
msToFormat = 1000 * 60 * 60;
|
||||||
formatted = formatMilliseconds(msToFormat);
|
formatted = formatMilliseconds(msToFormat);
|
||||||
expect(formatted).toBe('1 hour');
|
expect(formatted).toBe('1 h');
|
||||||
|
|
||||||
msToFormat = 1000 * 60 * 60 * 24;
|
msToFormat = 1000 * 60 * 60 * 24;
|
||||||
formatted = formatMilliseconds(msToFormat);
|
formatted = formatMilliseconds(msToFormat);
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ const conversions: Record<string, number> = {
|
|||||||
month: 1000 * 60 * 60 * 24 * 30,
|
month: 1000 * 60 * 60 * 24 * 30,
|
||||||
week: 1000 * 60 * 60 * 24 * 7,
|
week: 1000 * 60 * 60 * 24 * 7,
|
||||||
day: 1000 * 60 * 60 * 24,
|
day: 1000 * 60 * 60 * 24,
|
||||||
hour: 1000 * 60 * 60,
|
h: 1000 * 60 * 60,
|
||||||
minute: 1000 * 60,
|
m: 1000 * 60,
|
||||||
second: 1000,
|
s: 1000,
|
||||||
millisecond: 1,
|
ms: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const noPluralize = new Set(['ms', 's', 'm', 'h']);
|
||||||
|
|
||||||
// @TODO: display "~ 1 year/month"?
|
// @TODO: display "~ 1 year/month"?
|
||||||
export const formatMilliseconds = (milliseconds: number) => {
|
export const formatMilliseconds = (milliseconds: number) => {
|
||||||
let value = 1;
|
let value = 1;
|
||||||
let unit = 'millisecond';
|
let unit = 'ms';
|
||||||
|
|
||||||
for (unit in conversions) {
|
for (unit in conversions) {
|
||||||
if (milliseconds >= conversions[unit]) {
|
if (milliseconds >= conversions[unit]) {
|
||||||
@@ -45,7 +47,8 @@ export const formatMilliseconds = (milliseconds: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unitString = value === 1 ? unit : unit + 's';
|
const plural = value !== 1 && !noPluralize.has(unit);
|
||||||
|
const unitString = plural ? unit + 's' : unit;
|
||||||
|
|
||||||
return `${value} ${unitString}`;
|
return `${value} ${unitString}`;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user