mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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": [
|
||||
[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>
|
||||
|
||||
64
public/app/plugins/panel/heatmap/renderHistogram.tsx
Normal file
64
public/app/plugins/panel/heatmap/renderHistogram.tsx
Normal file
@@ -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