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": [ "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);
}
}
Loading…
Cancel
Save