HeatmapTooltip: Refactor to simplify upcoming PRs (#78436)
This commit is contained in:
@@ -1131,14 +1131,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/core/components/CloseButton/CloseButton.tsx:5381": [
|
"public/app/core/components/CloseButton/CloseButton.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/core/components/ColorScale/ColorScale.tsx:5381": [
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "5"]
|
|
||||||
],
|
|
||||||
"public/app/core/components/Divider.tsx:5381": [
|
"public/app/core/components/Divider.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||||
|
|||||||
@@ -128,40 +128,40 @@ function clampPercent100(v: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
||||||
scaleWrapper: css`
|
scaleWrapper: css({
|
||||||
width: 100%;
|
width: '100%',
|
||||||
font-size: 11px;
|
fontSize: '11px',
|
||||||
opacity: 1;
|
opacity: 1,
|
||||||
`,
|
}),
|
||||||
scaleGradient: css`
|
scaleGradient: css({
|
||||||
background: linear-gradient(90deg, ${colors.join()});
|
background: `linear-gradient(90deg, ${colors.join()})`,
|
||||||
height: 10px;
|
height: '10px',
|
||||||
pointer-events: none;
|
pointerEvents: 'none',
|
||||||
`,
|
}),
|
||||||
legendValues: css`
|
legendValues: css({
|
||||||
display: flex;
|
display: 'flex',
|
||||||
justify-content: space-between;
|
justifyContent: 'space-between',
|
||||||
pointer-events: none;
|
pointerEvents: 'none',
|
||||||
`,
|
}),
|
||||||
hoverValue: css`
|
hoverValue: css({
|
||||||
position: absolute;
|
position: 'absolute',
|
||||||
margin-top: -14px;
|
marginTop: '-14px',
|
||||||
padding: 3px 15px;
|
padding: '3px 15px',
|
||||||
background: ${theme.colors.background.primary};
|
background: theme.colors.background.primary,
|
||||||
transform: translateX(-50%);
|
transform: 'translateX(-50%)',
|
||||||
`,
|
}),
|
||||||
followerContainer: css`
|
followerContainer: css({
|
||||||
position: relative;
|
position: 'relative',
|
||||||
pointer-events: none;
|
pointerEvents: 'none',
|
||||||
white-space: nowrap;
|
whiteSpace: 'nowrap',
|
||||||
`,
|
}),
|
||||||
follower: css`
|
follower: css({
|
||||||
position: absolute;
|
position: 'absolute',
|
||||||
height: 14px;
|
height: '14px',
|
||||||
width: 14px;
|
width: '14px',
|
||||||
border-radius: 50%;
|
borderRadius: theme.shape.radius.default,
|
||||||
transform: translateX(-50%) translateY(-50%);
|
transform: 'translateX(-50%) translateY(-50%)',
|
||||||
border: 2px solid ${theme.colors.text.primary};
|
border: `2px solid ${theme.colors.text.primary}`,
|
||||||
margin-top: 5px;
|
marginTop: '5px',
|
||||||
`,
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,24 +21,27 @@ export interface Props {
|
|||||||
sortOrder?: SortOrder;
|
sortOrder?: SortOrder;
|
||||||
mode?: TooltipDisplayMode | null;
|
mode?: TooltipDisplayMode | null;
|
||||||
header?: string;
|
header?: string;
|
||||||
|
padding?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DisplayValue {
|
export interface DisplayValue {
|
||||||
name: string;
|
name: string;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
valueString: string;
|
valueString: string;
|
||||||
highlight: boolean;
|
highlight: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, header = undefined }: Props) => {
|
export function getDisplayValuesAndLinks(
|
||||||
const styles = useStyles2(getStyles);
|
data: DataFrame,
|
||||||
|
rowIndex: number,
|
||||||
if (!data || rowIndex == null) {
|
columnIndex?: number | null,
|
||||||
return null;
|
sortOrder?: SortOrder,
|
||||||
}
|
mode?: TooltipDisplayMode | null
|
||||||
|
) {
|
||||||
const fields = data.fields.map((f, idx) => {
|
const fields = data.fields.map((f, idx) => {
|
||||||
return { ...f, hovered: idx === columnIndex };
|
return { ...f, hovered: idx === columnIndex };
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleFields = fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
|
const visibleFields = fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
|
||||||
const traceIDField = visibleFields.find((field) => field.name === 'traceID') || fields[0];
|
const traceIDField = visibleFields.find((field) => field.name === 'traceID') || fields[0];
|
||||||
const orderedVisibleFields = [];
|
const orderedVisibleFields = [];
|
||||||
@@ -89,6 +92,24 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he
|
|||||||
displayValues.sort((a, b) => arrayUtils.sortValues(sortOrder)(a.value, b.value));
|
displayValues.sort((a, b) => arrayUtils.sortValues(sortOrder)(a.value, b.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { displayValues, links };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, header, padding = 0 }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles, padding);
|
||||||
|
|
||||||
|
if (!data || rowIndex == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispValuesAndLinks = getDisplayValuesAndLinks(data, rowIndex, columnIndex, sortOrder, mode);
|
||||||
|
|
||||||
|
if (dispValuesAndLinks == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { displayValues, links } = dispValuesAndLinks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{header && (
|
{header && (
|
||||||
@@ -119,9 +140,10 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2, padding = 0) => {
|
||||||
return {
|
return {
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
|
padding: ${padding}px;
|
||||||
background: ${theme.components.tooltip.background};
|
background: ${theme.components.tooltip.background};
|
||||||
border-radius: ${theme.shape.borderRadius(2)};
|
border-radius: ${theme.shape.borderRadius(2)};
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataFrameType,
|
DataFrameType,
|
||||||
@@ -19,6 +20,7 @@ import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/tra
|
|||||||
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
||||||
|
|
||||||
import { HeatmapData } from './fields';
|
import { HeatmapData } from './fields';
|
||||||
|
import { renderHistogram } from './renderHistogram';
|
||||||
import { HeatmapHoverEvent } from './utils';
|
import { HeatmapHoverEvent } from './utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -37,8 +39,13 @@ export const HeatmapHoverView = (props: Props) => {
|
|||||||
return <HeatmapHoverCell {...props} />;
|
return <HeatmapHoverCell {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars }: Props) => {
|
const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, replaceVars }: Props) => {
|
||||||
const index = hover.dataIdx;
|
const index = hover.dataIdx;
|
||||||
|
|
||||||
|
const [isSparse] = useState(
|
||||||
|
() => data.heatmap?.meta?.type === DataFrameType.HeatmapCells && !isHeatmapCellsDense(data.heatmap)
|
||||||
|
);
|
||||||
|
|
||||||
const xField = data.heatmap?.fields[0];
|
const xField = data.heatmap?.fields[0];
|
||||||
const yField = data.heatmap?.fields[1];
|
const yField = data.heatmap?.fields[1];
|
||||||
const countField = data.heatmap?.fields[2];
|
const countField = data.heatmap?.fields[2];
|
||||||
@@ -151,78 +158,21 @@ const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars
|
|||||||
|
|
||||||
let can = useRef<HTMLCanvasElement>(null);
|
let can = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
let histCssWidth = 150;
|
let histCssWidth = 264;
|
||||||
let histCssHeight = 50;
|
let histCssHeight = 64;
|
||||||
let histCanWidth = Math.round(histCssWidth * devicePixelRatio);
|
let histCanWidth = Math.round(histCssWidth * uPlot.pxRatio);
|
||||||
let histCanHeight = Math.round(histCssHeight * devicePixelRatio);
|
let histCanHeight = Math.round(histCssHeight * uPlot.pxRatio);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (showHistogram) {
|
if (showHistogram && xVals != null && countVals != null) {
|
||||||
let histCtx = can.current?.getContext('2d');
|
renderHistogram(can, histCanWidth, histCanHeight, xVals, countVals, index, data.yBucketCount!);
|
||||||
|
|
||||||
if (histCtx && xVals && yVals && countVals) {
|
|
||||||
let fromIdx = index;
|
|
||||||
|
|
||||||
while (xVals[fromIdx--] === xVals[index]) {}
|
|
||||||
|
|
||||||
fromIdx++;
|
|
||||||
|
|
||||||
let toIdx = fromIdx + data.yBucketCount!;
|
|
||||||
|
|
||||||
let maxCount = 0;
|
|
||||||
|
|
||||||
let i = fromIdx;
|
|
||||||
while (i < toIdx) {
|
|
||||||
let c = countVals[i];
|
|
||||||
maxCount = Math.max(maxCount, c);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pHov = new Path2D();
|
|
||||||
let pRest = new Path2D();
|
|
||||||
|
|
||||||
i = fromIdx;
|
|
||||||
let j = 0;
|
|
||||||
while (i < toIdx) {
|
|
||||||
let c = countVals[i];
|
|
||||||
|
|
||||||
if (c > 0) {
|
|
||||||
let pctY = c / maxCount;
|
|
||||||
let pctX = j / (data.yBucketCount! + 1);
|
|
||||||
|
|
||||||
let p = i === index ? pHov : pRest;
|
|
||||||
|
|
||||||
p.rect(
|
|
||||||
Math.round(histCanWidth * pctX),
|
|
||||||
Math.round(histCanHeight * (1 - pctY)),
|
|
||||||
Math.round(histCanWidth / data.yBucketCount!),
|
|
||||||
Math.round(histCanHeight * pctY)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
histCtx.clearRect(0, 0, histCanWidth, histCanHeight);
|
|
||||||
|
|
||||||
histCtx.fillStyle = '#ffffff80';
|
|
||||||
histCtx.fill(pRest);
|
|
||||||
|
|
||||||
histCtx.fillStyle = '#ff000080';
|
|
||||||
histCtx.fill(pHov);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[index]
|
[index]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isSparse] = useState(
|
|
||||||
() => data.heatmap?.meta?.type === DataFrameType.HeatmapCells && !isHeatmapCellsDense(data.heatmap)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSparse) {
|
if (isSparse) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -258,7 +208,7 @@ const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars
|
|||||||
width={histCanWidth}
|
width={histCanWidth}
|
||||||
height={histCanHeight}
|
height={histCanHeight}
|
||||||
ref={can}
|
ref={can}
|
||||||
style={{ width: histCanWidth + 'px', height: histCanHeight + 'px' }}
|
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
export function renderHistogram(
|
||||||
|
can: React.RefObject<HTMLCanvasElement>,
|
||||||
|
histCanWidth: number,
|
||||||
|
histCanHeight: number,
|
||||||
|
xVals: number[],
|
||||||
|
countVals: number[],
|
||||||
|
index: number,
|
||||||
|
yBucketCount: number
|
||||||
|
) {
|
||||||
|
let histCtx = can.current?.getContext('2d');
|
||||||
|
|
||||||
|
if (histCtx != null) {
|
||||||
|
let fromIdx = index;
|
||||||
|
|
||||||
|
while (xVals[fromIdx--] === xVals[index]) {}
|
||||||
|
|
||||||
|
fromIdx++;
|
||||||
|
|
||||||
|
let toIdx = fromIdx + yBucketCount;
|
||||||
|
|
||||||
|
let maxCount = 0;
|
||||||
|
|
||||||
|
let i = fromIdx;
|
||||||
|
while (i < toIdx) {
|
||||||
|
let c = countVals[i];
|
||||||
|
maxCount = Math.max(maxCount, c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pHov = new Path2D();
|
||||||
|
let pRest = new Path2D();
|
||||||
|
|
||||||
|
i = fromIdx;
|
||||||
|
let j = 0;
|
||||||
|
while (i < toIdx) {
|
||||||
|
let c = countVals[i];
|
||||||
|
|
||||||
|
if (c > 0) {
|
||||||
|
let pctY = c / maxCount;
|
||||||
|
let pctX = j / (yBucketCount + 1);
|
||||||
|
|
||||||
|
let p = i === index ? pHov : pRest;
|
||||||
|
|
||||||
|
p.rect(
|
||||||
|
Math.round(histCanWidth * pctX),
|
||||||
|
Math.round(histCanHeight * (1 - pctY)),
|
||||||
|
Math.round(histCanWidth / yBucketCount),
|
||||||
|
Math.round(histCanHeight * pctY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
histCtx.clearRect(0, 0, histCanWidth, histCanHeight);
|
||||||
|
|
||||||
|
histCtx.fillStyle = '#2E3036';
|
||||||
|
histCtx.fill(pRest);
|
||||||
|
|
||||||
|
histCtx.fillStyle = '#5794F2';
|
||||||
|
histCtx.fill(pHov);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user