HeatmapTooltip: Refactor to simplify upcoming PRs (#78436)

pull/78496/head^2
Leon Sorokin 2 years ago committed by GitHub
parent 0e44438d08
commit d1cffdc58a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .betterer.results
  2. 72
      public/app/core/components/ColorScale/ColorScale.tsx
  3. 38
      public/app/features/visualization/data-hover/DataHoverView.tsx
  4. 80
      public/app/plugins/panel/heatmap/HeatmapHoverView.tsx
  5. 64
      public/app/plugins/panel/heatmap/renderHistogram.tsx

@ -1131,14 +1131,6 @@ exports[`better eslint`] = {
"public/app/core/components/CloseButton/CloseButton.tsx:5381": [
[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": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]

@ -128,40 +128,40 @@ function clampPercent100(v: number) {
}
const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
scaleWrapper: css`
width: 100%;
font-size: 11px;
opacity: 1;
`,
scaleGradient: css`
background: linear-gradient(90deg, ${colors.join()});
height: 10px;
pointer-events: none;
`,
legendValues: css`
display: flex;
justify-content: space-between;
pointer-events: none;
`,
hoverValue: css`
position: absolute;
margin-top: -14px;
padding: 3px 15px;
background: ${theme.colors.background.primary};
transform: translateX(-50%);
`,
followerContainer: css`
position: relative;
pointer-events: none;
white-space: nowrap;
`,
follower: css`
position: absolute;
height: 14px;
width: 14px;
border-radius: 50%;
transform: translateX(-50%) translateY(-50%);
border: 2px solid ${theme.colors.text.primary};
margin-top: 5px;
`,
scaleWrapper: css({
width: '100%',
fontSize: '11px',
opacity: 1,
}),
scaleGradient: css({
background: `linear-gradient(90deg, ${colors.join()})`,
height: '10px',
pointerEvents: 'none',
}),
legendValues: css({
display: 'flex',
justifyContent: 'space-between',
pointerEvents: 'none',
}),
hoverValue: css({
position: 'absolute',
marginTop: '-14px',
padding: '3px 15px',
background: theme.colors.background.primary,
transform: 'translateX(-50%)',
}),
followerContainer: css({
position: 'relative',
pointerEvents: 'none',
whiteSpace: 'nowrap',
}),
follower: css({
position: 'absolute',
height: '14px',
width: '14px',
borderRadius: theme.shape.radius.default,
transform: 'translateX(-50%) translateY(-50%)',
border: `2px solid ${theme.colors.text.primary}`,
marginTop: '5px',
}),
});

@ -21,24 +21,27 @@ export interface Props {
sortOrder?: SortOrder;
mode?: TooltipDisplayMode | null;
header?: string;
padding?: number;
}
interface DisplayValue {
export interface DisplayValue {
name: string;
value: unknown;
valueString: string;
highlight: boolean;
}
export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, header = undefined }: Props) => {
const styles = useStyles2(getStyles);
if (!data || rowIndex == null) {
return null;
}
export function getDisplayValuesAndLinks(
data: DataFrame,
rowIndex: number,
columnIndex?: number | null,
sortOrder?: SortOrder,
mode?: TooltipDisplayMode | null
) {
const fields = data.fields.map((f, idx) => {
return { ...f, hovered: idx === columnIndex };
});
const visibleFields = fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
const traceIDField = visibleFields.find((field) => field.name === 'traceID') || fields[0];
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));
}
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 (
<div className={styles.wrapper}>
{header && (
@ -119,9 +140,10 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
const getStyles = (theme: GrafanaTheme2, padding = 0) => {
return {
wrapper: css`
padding: ${padding}px;
background: ${theme.components.tooltip.background};
border-radius: ${theme.shape.borderRadius(2)};
`,

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
import {
DataFrameType,
@ -19,6 +20,7 @@ import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/tra
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
import { HeatmapData } from './fields';
import { renderHistogram } from './renderHistogram';
import { HeatmapHoverEvent } from './utils';
type Props = {
@ -37,8 +39,13 @@ export const HeatmapHoverView = (props: 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 [isSparse] = useState(
() => data.heatmap?.meta?.type === DataFrameType.HeatmapCells && !isHeatmapCellsDense(data.heatmap)
);
const xField = data.heatmap?.fields[0];
const yField = data.heatmap?.fields[1];
const countField = data.heatmap?.fields[2];
@ -151,78 +158,21 @@ const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars
let can = useRef<HTMLCanvasElement>(null);
let histCssWidth = 150;
let histCssHeight = 50;
let histCanWidth = Math.round(histCssWidth * devicePixelRatio);
let histCanHeight = Math.round(histCssHeight * devicePixelRatio);
let histCssWidth = 264;
let histCssHeight = 64;
let histCanWidth = Math.round(histCssWidth * uPlot.pxRatio);
let histCanHeight = Math.round(histCssHeight * uPlot.pxRatio);
useEffect(
() => {
if (showHistogram) {
let histCtx = can.current?.getContext('2d');
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);
}
if (showHistogram && xVals != null && countVals != null) {
renderHistogram(can, histCanWidth, histCanHeight, xVals, countVals, index, data.yBucketCount!);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[index]
);
const [isSparse] = useState(
() => data.heatmap?.meta?.type === DataFrameType.HeatmapCells && !isHeatmapCellsDense(data.heatmap)
);
if (isSparse) {
return (
<div>
@ -258,7 +208,7 @@ const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars
width={histCanWidth}
height={histCanHeight}
ref={can}
style={{ width: histCanWidth + 'px', height: histCanHeight + 'px' }}
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
/>
)}
<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);
}
}
Loading…
Cancel
Save