mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tooltip: Improved Heatmap tooltip (#75712)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
parent
5a80962de9
commit
e361839261
@ -1,5 +1,5 @@
|
||||
// BETTERER RESULTS V2.
|
||||
//
|
||||
//
|
||||
// If this file contains merge conflicts, use `betterer merge` to automatically resolve them:
|
||||
// https://phenomnomnominal.github.io/betterer/docs/results-file/#merge
|
||||
//
|
||||
@ -6544,8 +6544,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/plugins/panel/heatmap/HeatmapPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/heatmap/migrations.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
@ -124,10 +124,11 @@ Controls legend options
|
||||
|
||||
Controls tooltip options
|
||||
|
||||
| Property | Type | Required | Default | Description |
|
||||
|--------------|---------|----------|---------|----------------------------------------------------------------|
|
||||
| `show` | boolean | **Yes** | | Controls if the tooltip is shown |
|
||||
| `yHistogram` | boolean | No | | Controls if the tooltip shows a histogram of the y-axis values |
|
||||
| Property | Type | Required | Default | Description |
|
||||
|------------------|---------|----------|---------|----------------------------------------------------------------|
|
||||
| `show` | boolean | **Yes** | | Controls if the tooltip is shown |
|
||||
| `showColorScale` | boolean | No | | Controls if the tooltip shows a color scale in header |
|
||||
| `yHistogram` | boolean | No | | Controls if the tooltip shows a histogram of the y-axis values |
|
||||
|
||||
### Options
|
||||
|
||||
|
@ -114,6 +114,7 @@ Use these settings to refine your visualization.
|
||||
|
||||
- **Show tooltip -** Show heatmap tooltip.
|
||||
- **Show Histogram -** Show a Y-axis histogram on the tooltip. A histogram represents the distribution of the bucket values for a specific timestamp.
|
||||
- **Show color scale -** Show a color scale on the tooltip. The color scale represents the mapping between bucket value and color. This option is configurable when you enable the `newVizTooltips` feature flag.
|
||||
|
||||
### Legend
|
||||
|
||||
|
@ -133,6 +133,10 @@ export interface HeatmapTooltip {
|
||||
* Controls if the tooltip is shown
|
||||
*/
|
||||
show: boolean;
|
||||
/**
|
||||
* Controls if the tooltip shows a color scale in header
|
||||
*/
|
||||
showColorScale?: boolean;
|
||||
/**
|
||||
* Controls if the tooltip shows a histogram of the y-axis values
|
||||
*/
|
||||
@ -264,6 +268,7 @@ export const defaultOptions: Partial<Options> = {
|
||||
tooltip: {
|
||||
show: true,
|
||||
yHistogram: false,
|
||||
showColorScale: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -64,8 +64,8 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useSto
|
||||
{display && (
|
||||
<div className={styles.followerContainer}>
|
||||
<div className={styles.legendValues}>
|
||||
<span>{display(min)}</span>
|
||||
<span>{display(max)}</span>
|
||||
<span className={styles.disabled}>{display(min)}</span>
|
||||
<span className={styles.disabled}>{display(max)}</span>
|
||||
</div>
|
||||
{percent != null && (scaleHover.isShown || hoverValue !== undefined) && (
|
||||
<span className={styles.hoverValue} style={{ left: `${percent}%` }}>
|
||||
@ -135,8 +135,9 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
||||
}),
|
||||
scaleGradient: css({
|
||||
background: `linear-gradient(90deg, ${colors.join()})`,
|
||||
height: '10px',
|
||||
height: '9px',
|
||||
pointerEvents: 'none',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
}),
|
||||
legendValues: css({
|
||||
display: 'flex',
|
||||
@ -147,7 +148,6 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
||||
position: 'absolute',
|
||||
marginTop: '-14px',
|
||||
padding: '3px 15px',
|
||||
background: theme.colors.background.primary,
|
||||
transform: 'translateX(-50%)',
|
||||
}),
|
||||
followerContainer: css({
|
||||
@ -157,11 +157,14 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
||||
}),
|
||||
follower: css({
|
||||
position: 'absolute',
|
||||
height: '14px',
|
||||
width: '14px',
|
||||
height: '13px',
|
||||
width: '13px',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
transform: 'translateX(-50%) translateY(-50%)',
|
||||
border: `2px solid ${theme.colors.text.primary}`,
|
||||
marginTop: '5px',
|
||||
top: '5px',
|
||||
}),
|
||||
disabled: css({
|
||||
color: theme.colors.text.disabled,
|
||||
}),
|
||||
});
|
||||
|
@ -1,54 +1,86 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
DataFrameType,
|
||||
Field,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
LinkModel,
|
||||
TimeRange,
|
||||
GrafanaTheme2,
|
||||
getLinksSupplier,
|
||||
InterpolateFunction,
|
||||
ScopedVars,
|
||||
PanelData,
|
||||
LinkModel,
|
||||
Field,
|
||||
FieldType,
|
||||
} from '@grafana/data';
|
||||
import { HeatmapCellLayout } from '@grafana/schema';
|
||||
import { LinkButton, VerticalGroup } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
|
||||
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
|
||||
import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
|
||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
||||
|
||||
import { HeatmapData } from './fields';
|
||||
import { renderHistogram } from './renderHistogram';
|
||||
import { HeatmapHoverEvent } from './utils';
|
||||
import { getSparseCellMinMax, formatMilliseconds, getFieldFromData, getHoverCellColor } from './tooltip/utils';
|
||||
|
||||
type Props = {
|
||||
data: HeatmapData;
|
||||
hover: HeatmapHoverEvent;
|
||||
interface Props {
|
||||
dataIdxs: Array<number | null>;
|
||||
seriesIdx: number | null | undefined;
|
||||
dataRef: React.MutableRefObject<HeatmapData>;
|
||||
showHistogram?: boolean;
|
||||
timeRange: TimeRange;
|
||||
showColorScale?: boolean;
|
||||
isPinned: boolean;
|
||||
dismiss: () => void;
|
||||
canAnnotate: boolean;
|
||||
panelData: PanelData;
|
||||
replaceVars: InterpolateFunction;
|
||||
scopedVars: ScopedVars[];
|
||||
};
|
||||
}
|
||||
|
||||
export const HeatmapHoverView = (props: Props) => {
|
||||
if (props.hover.seriesIdx === 2) {
|
||||
return <DataHoverView data={props.data.exemplars} rowIndex={props.hover.dataIdx} header={'Exemplar'} />;
|
||||
if (props.seriesIdx === 2) {
|
||||
return (
|
||||
<DataHoverView
|
||||
data={props.dataRef.current!.exemplars}
|
||||
rowIndex={props.dataIdxs[2]}
|
||||
header={'Exemplar'}
|
||||
padding={8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <HeatmapHoverCell {...props} />;
|
||||
};
|
||||
|
||||
const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, replaceVars }: Props) => {
|
||||
const index = hover.dataIdx;
|
||||
const HeatmapHoverCell = ({
|
||||
dataIdxs,
|
||||
dataRef,
|
||||
showHistogram,
|
||||
isPinned,
|
||||
canAnnotate,
|
||||
panelData,
|
||||
showColorScale = false,
|
||||
scopedVars,
|
||||
replaceVars,
|
||||
dismiss,
|
||||
}: Props) => {
|
||||
const index = dataIdxs[1]!;
|
||||
const data = dataRef.current;
|
||||
|
||||
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];
|
||||
const xField = getFieldFromData(data.heatmap!, 'x', isSparse)!;
|
||||
const yField = getFieldFromData(data.heatmap!, 'y', isSparse)!;
|
||||
const countField = getFieldFromData(data.heatmap!, 'count', isSparse)!;
|
||||
|
||||
const xDisp = (v: number) => {
|
||||
if (xField?.display) {
|
||||
@ -62,9 +94,9 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl
|
||||
return `${v}`;
|
||||
};
|
||||
|
||||
const xVals = xField?.values;
|
||||
const yVals = yField?.values;
|
||||
const countVals = countField?.values;
|
||||
const xVals = xField.values;
|
||||
const yVals = yField.values;
|
||||
const countVals = countField.values;
|
||||
|
||||
// labeled buckets
|
||||
const meta = readHeatmapRowsCustomMeta(data.heatmap);
|
||||
@ -72,56 +104,62 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl
|
||||
|
||||
const yValueIdx = index % data.yBucketCount! ?? 0;
|
||||
|
||||
let interval = xField?.config.interval;
|
||||
|
||||
let yBucketMin: string;
|
||||
let yBucketMax: string;
|
||||
|
||||
let nonNumericOrdinalDisplay: string | undefined = undefined;
|
||||
|
||||
if (meta.yOrdinalDisplay) {
|
||||
const yMinIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx - 1 : yValueIdx;
|
||||
const yMaxIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx : yValueIdx + 1;
|
||||
yBucketMin = yMinIdx < 0 ? meta.yMinDisplay! : `${meta.yOrdinalDisplay[yMinIdx]}`;
|
||||
yBucketMax = `${meta.yOrdinalDisplay[yMaxIdx]}`;
|
||||
|
||||
// e.g. "pod-xyz123"
|
||||
if (!meta.yOrdinalLabel || Number.isNaN(+meta.yOrdinalLabel[0])) {
|
||||
nonNumericOrdinalDisplay = data.yLayout === HeatmapCellLayout.le ? yBucketMax : yBucketMin;
|
||||
}
|
||||
} else {
|
||||
const value = yVals?.[yValueIdx];
|
||||
|
||||
if (data.yLayout === HeatmapCellLayout.le) {
|
||||
yBucketMax = `${value}`;
|
||||
|
||||
if (data.yLog) {
|
||||
let logFn = data.yLog === 2 ? Math.log2 : Math.log10;
|
||||
let exp = logFn(value) - 1 / data.yLogSplit!;
|
||||
yBucketMin = `${data.yLog ** exp}`;
|
||||
} else {
|
||||
yBucketMin = `${value - data.yBucketSize!}`;
|
||||
}
|
||||
} else {
|
||||
yBucketMin = `${value}`;
|
||||
|
||||
if (data.yLog) {
|
||||
let logFn = data.yLog === 2 ? Math.log2 : Math.log10;
|
||||
let exp = logFn(value) + 1 / data.yLogSplit!;
|
||||
yBucketMax = `${data.yLog ** exp}`;
|
||||
} else {
|
||||
yBucketMax = `${value + data.yBucketSize!}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let xBucketMin: number;
|
||||
let xBucketMax: number;
|
||||
|
||||
if (data.xLayout === HeatmapCellLayout.le) {
|
||||
xBucketMax = xVals?.[index];
|
||||
xBucketMin = xBucketMax - data.xBucketSize!;
|
||||
let nonNumericOrdinalDisplay: string | undefined = undefined;
|
||||
|
||||
if (isSparse) {
|
||||
({ xBucketMin, xBucketMax, yBucketMin, yBucketMax } = getSparseCellMinMax(data!, index));
|
||||
} else {
|
||||
xBucketMin = xVals?.[index];
|
||||
xBucketMax = xBucketMin + data.xBucketSize!;
|
||||
if (meta.yOrdinalDisplay) {
|
||||
const yMinIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx - 1 : yValueIdx;
|
||||
const yMaxIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx : yValueIdx + 1;
|
||||
yBucketMin = yMinIdx < 0 ? meta.yMinDisplay! : `${meta.yOrdinalDisplay[yMinIdx]}`;
|
||||
yBucketMax = `${meta.yOrdinalDisplay[yMaxIdx]}`;
|
||||
|
||||
// e.g. "pod-xyz123"
|
||||
if (!meta.yOrdinalLabel || Number.isNaN(+meta.yOrdinalLabel[0])) {
|
||||
nonNumericOrdinalDisplay = data.yLayout === HeatmapCellLayout.le ? yBucketMax : yBucketMin;
|
||||
}
|
||||
} else {
|
||||
const value = yVals?.[yValueIdx];
|
||||
|
||||
if (data.yLayout === HeatmapCellLayout.le) {
|
||||
yBucketMax = `${value}`;
|
||||
|
||||
if (data.yLog) {
|
||||
let logFn = data.yLog === 2 ? Math.log2 : Math.log10;
|
||||
let exp = logFn(value) - 1 / data.yLogSplit!;
|
||||
yBucketMin = `${data.yLog ** exp}`;
|
||||
} else {
|
||||
yBucketMin = `${value - data.yBucketSize!}`;
|
||||
}
|
||||
} else {
|
||||
yBucketMin = `${value}`;
|
||||
|
||||
if (data.yLog) {
|
||||
let logFn = data.yLog === 2 ? Math.log2 : Math.log10;
|
||||
let exp = logFn(value) + 1 / data.yLogSplit!;
|
||||
yBucketMax = `${data.yLog ** exp}`;
|
||||
} else {
|
||||
yBucketMax = `${value + data.yBucketSize!}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.xLayout === HeatmapCellLayout.le) {
|
||||
xBucketMax = xVals[index];
|
||||
xBucketMin = xBucketMax - data.xBucketSize!;
|
||||
} else {
|
||||
xBucketMin = xVals[index];
|
||||
xBucketMax = xBucketMin + data.xBucketSize!;
|
||||
}
|
||||
}
|
||||
|
||||
const count = countVals?.[index];
|
||||
@ -173,67 +211,109 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl
|
||||
[index]
|
||||
);
|
||||
|
||||
if (isSparse) {
|
||||
return (
|
||||
<div>
|
||||
<DataHoverView data={data.heatmap} rowIndex={index} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { cellColor, colorPalette } = getHoverCellColor(data, index);
|
||||
|
||||
const renderYBucket = () => {
|
||||
const getLabelValue = (): LabelValue[] => {
|
||||
return [
|
||||
{
|
||||
label: getFieldDisplayName(countField, data.heatmap),
|
||||
value: data.display!(count),
|
||||
color: cellColor ?? '#FFF',
|
||||
colorIndicator: ColorIndicator.value,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getHeaderLabel = (): LabelValue => {
|
||||
if (nonNumericOrdinalDisplay) {
|
||||
return <div>Name: {nonNumericOrdinalDisplay}</div>;
|
||||
return { label: 'Name', value: nonNumericOrdinalDisplay };
|
||||
}
|
||||
|
||||
switch (data.yLayout) {
|
||||
case HeatmapCellLayout.unknown:
|
||||
return <div>{yDisp(yBucketMin)}</div>;
|
||||
return { label: '', value: yDisp(yBucketMin) };
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return {
|
||||
label: 'Bucket',
|
||||
value: `${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>{xDisp(xBucketMin)}</div>
|
||||
{data.xLayout !== HeatmapCellLayout.unknown && <div>{xDisp(xBucketMax)}</div>}
|
||||
</div>
|
||||
{showHistogram && (
|
||||
// Color scale
|
||||
const getCustomValueDisplay = (): ReactElement | null => {
|
||||
if (colorPalette && showColorScale) {
|
||||
return (
|
||||
<ColorScale
|
||||
colorPalette={colorPalette}
|
||||
min={data.heatmapColors?.minValue!}
|
||||
max={data.heatmapColors?.maxValue!}
|
||||
display={data.display}
|
||||
hoverValue={count}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
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' }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{renderYBucket()}
|
||||
<div>
|
||||
{getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)}
|
||||
</div>
|
||||
</div>
|
||||
{links.length > 0 && (
|
||||
<VerticalGroup>
|
||||
{links.map((link, i) => (
|
||||
<LinkButton
|
||||
key={i}
|
||||
icon={'external-link-alt'}
|
||||
target={link.target}
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
fill="text"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{link.title}
|
||||
</LinkButton>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// @TODO remove this when adding annotations support
|
||||
canAnnotate = false;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader
|
||||
headerLabel={getHeaderLabel()}
|
||||
keyValuePairs={getLabelValue()}
|
||||
customValueDisplay={getCustomValueDisplay()}
|
||||
/>
|
||||
<VizTooltipContent contentLabelValue={getContentLabelValue()} customContent={getCustomContent()} />
|
||||
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={canAnnotate} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '280px',
|
||||
}),
|
||||
});
|
||||
|
239
public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx
Normal file
239
public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx
Normal file
@ -0,0 +1,239 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
DataFrameType,
|
||||
Field,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
LinkModel,
|
||||
TimeRange,
|
||||
getLinksSupplier,
|
||||
InterpolateFunction,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { HeatmapCellLayout } from '@grafana/schema';
|
||||
import { LinkButton, VerticalGroup } from '@grafana/ui';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
|
||||
|
||||
import { HeatmapData } from './fields';
|
||||
import { renderHistogram } from './renderHistogram';
|
||||
import { HeatmapHoverEvent } from './utils';
|
||||
|
||||
type Props = {
|
||||
data: HeatmapData;
|
||||
hover: HeatmapHoverEvent;
|
||||
showHistogram?: boolean;
|
||||
timeRange: TimeRange;
|
||||
replaceVars: InterpolateFunction;
|
||||
scopedVars: ScopedVars[];
|
||||
};
|
||||
|
||||
export const HeatmapHoverView = (props: Props) => {
|
||||
if (props.hover.seriesIdx === 2) {
|
||||
return <DataHoverView data={props.data.exemplars} rowIndex={props.hover.dataIdx} header={'Exemplar'} />;
|
||||
}
|
||||
return <HeatmapHoverCell {...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];
|
||||
|
||||
const xDisp = (v: number) => {
|
||||
if (xField?.display) {
|
||||
return formattedValueToString(xField.display(v));
|
||||
}
|
||||
if (xField?.type === FieldType.time) {
|
||||
const tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const dashboard = getDashboardSrv().getCurrent();
|
||||
return dashboard?.formatDate(v, tooltipTimeFormat);
|
||||
}
|
||||
return `${v}`;
|
||||
};
|
||||
|
||||
const xVals = xField?.values;
|
||||
const yVals = yField?.values;
|
||||
const countVals = countField?.values;
|
||||
|
||||
// labeled buckets
|
||||
const meta = readHeatmapRowsCustomMeta(data.heatmap);
|
||||
const yDisp = yField?.display ? (v: string) => formattedValueToString(yField.display!(v)) : (v: string) => `${v}`;
|
||||
|
||||
const yValueIdx = index % data.yBucketCount! ?? 0;
|
||||
|
||||
let yBucketMin: string;
|
||||
let yBucketMax: string;
|
||||
|
||||
let nonNumericOrdinalDisplay: string | undefined = undefined;
|
||||
|
||||
if (meta.yOrdinalDisplay) {
|
||||
const yMinIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx - 1 : yValueIdx;
|
||||
const yMaxIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx : yValueIdx + 1;
|
||||
yBucketMin = yMinIdx < 0 ? meta.yMinDisplay! : `${meta.yOrdinalDisplay[yMinIdx]}`;
|
||||
yBucketMax = `${meta.yOrdinalDisplay[yMaxIdx]}`;
|
||||
|
||||
// e.g. "pod-xyz123"
|
||||
if (!meta.yOrdinalLabel || Number.isNaN(+meta.yOrdinalLabel[0])) {
|
||||
nonNumericOrdinalDisplay = data.yLayout === HeatmapCellLayout.le ? yBucketMax : yBucketMin;
|
||||
}
|
||||
} else {
|
||||
const value = yVals?.[yValueIdx];
|
||||
|
||||
if (data.yLayout === HeatmapCellLayout.le) {
|
||||
yBucketMax = `${value}`;
|
||||
|
||||
if (data.yLog) {
|
||||
let logFn = data.yLog === 2 ? Math.log2 : Math.log10;
|
||||
let exp = logFn(value) - 1 / data.yLogSplit!;
|
||||
yBucketMin = `${data.yLog ** exp}`;
|
||||
} else {
|
||||
yBucketMin = `${value - data.yBucketSize!}`;
|
||||
}
|
||||
} else {
|
||||
yBucketMin = `${value}`;
|
||||
|
||||
if (data.yLog) {
|
||||
let logFn = data.yLog === 2 ? Math.log2 : Math.log10;
|
||||
let exp = logFn(value) + 1 / data.yLogSplit!;
|
||||
yBucketMax = `${data.yLog ** exp}`;
|
||||
} else {
|
||||
yBucketMax = `${value + data.yBucketSize!}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let xBucketMin: number;
|
||||
let xBucketMax: number;
|
||||
|
||||
if (data.xLayout === HeatmapCellLayout.le) {
|
||||
xBucketMax = xVals?.[index];
|
||||
xBucketMin = xBucketMax - data.xBucketSize!;
|
||||
} else {
|
||||
xBucketMin = xVals?.[index];
|
||||
xBucketMax = xBucketMin + data.xBucketSize!;
|
||||
}
|
||||
|
||||
const count = countVals?.[index];
|
||||
|
||||
const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
|
||||
const links: Array<LinkModel<Field>> = [];
|
||||
const linkLookup = new Set<string>();
|
||||
|
||||
for (const field of visibleFields ?? []) {
|
||||
const hasLinks = field.config.links && field.config.links.length > 0;
|
||||
|
||||
if (hasLinks && data.heatmap) {
|
||||
const appropriateScopedVars = scopedVars.find(
|
||||
(scopedVar) =>
|
||||
scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay
|
||||
);
|
||||
|
||||
field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars);
|
||||
}
|
||||
|
||||
if (field.getLinks) {
|
||||
const value = field.values[index];
|
||||
const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value };
|
||||
|
||||
field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => {
|
||||
const key = `${link.title}/${link.href}`;
|
||||
if (!linkLookup.has(key)) {
|
||||
links.push(link);
|
||||
linkLookup.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let can = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
let histCssWidth = 264;
|
||||
let histCssHeight = 64;
|
||||
let histCanWidth = Math.round(histCssWidth * uPlot.pxRatio);
|
||||
let histCanHeight = Math.round(histCssHeight * uPlot.pxRatio);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (showHistogram && xVals != null && countVals != null) {
|
||||
renderHistogram(can, histCanWidth, histCanHeight, xVals, countVals, index, data.yBucketCount!);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[index]
|
||||
);
|
||||
|
||||
if (isSparse) {
|
||||
return (
|
||||
<div>
|
||||
<DataHoverView data={data.heatmap} rowIndex={index} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderYBucket = () => {
|
||||
if (nonNumericOrdinalDisplay) {
|
||||
return <div>Name: {nonNumericOrdinalDisplay}</div>;
|
||||
}
|
||||
|
||||
switch (data.yLayout) {
|
||||
case HeatmapCellLayout.unknown:
|
||||
return <div>{yDisp(yBucketMin)}</div>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>{xDisp(xBucketMin)}</div>
|
||||
{data.xLayout !== HeatmapCellLayout.unknown && <div>{xDisp(xBucketMax)}</div>}
|
||||
</div>
|
||||
{showHistogram && (
|
||||
<canvas
|
||||
width={histCanWidth}
|
||||
height={histCanHeight}
|
||||
ref={can}
|
||||
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{renderYBucket()}
|
||||
<div>
|
||||
{getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)}
|
||||
</div>
|
||||
</div>
|
||||
{links.length > 0 && (
|
||||
<VerticalGroup>
|
||||
{links.map((link, i) => (
|
||||
<LinkButton
|
||||
key={i}
|
||||
icon={'external-link-alt'}
|
||||
target={link.target}
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
fill="text"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{link.title}
|
||||
</LinkButton>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,12 +1,23 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { DataFrame, DataFrameType, Field, getLinksSupplier, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
import {
|
||||
DataFrame,
|
||||
DataFrameType,
|
||||
Field,
|
||||
getLinksSupplier,
|
||||
GrafanaTheme2,
|
||||
PanelProps,
|
||||
ScopedVars,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { config, PanelDataErrorView } from '@grafana/runtime';
|
||||
import { ScaleDistributionConfig } from '@grafana/schema';
|
||||
import {
|
||||
Portal,
|
||||
ScaleDistribution,
|
||||
TooltipPlugin2,
|
||||
ZoomPlugin,
|
||||
UPlotChart,
|
||||
usePanelContext,
|
||||
useStyles2,
|
||||
@ -14,11 +25,13 @@ import {
|
||||
VizLayout,
|
||||
VizTooltipContainer,
|
||||
} from '@grafana/ui';
|
||||
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
|
||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
|
||||
import { ExemplarModalHeader } from './ExemplarModalHeader';
|
||||
import { HeatmapHoverView } from './HeatmapHoverView';
|
||||
import { HeatmapHoverView as HeatmapHoverViewOld } from './HeatmapHoverViewOld';
|
||||
import { prepareHeatmapData } from './fields';
|
||||
import { quantizeScheme } from './palettes';
|
||||
import { Options } from './types';
|
||||
@ -41,10 +54,12 @@ export const HeatmapPanel = ({
|
||||
}: HeatmapPanelProps) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { sync } = usePanelContext();
|
||||
const { sync, canAddAnnotations } = usePanelContext();
|
||||
|
||||
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
|
||||
|
||||
// necessary for enabling datalinks in hover view
|
||||
let scopedVarsFromRawData = [];
|
||||
let scopedVarsFromRawData: ScopedVars[] = [];
|
||||
for (const series of data.series) {
|
||||
for (const field of series.fields) {
|
||||
if (field.state?.scopedVars) {
|
||||
@ -149,12 +164,6 @@ export const HeatmapPanel = ({
|
||||
eventBus,
|
||||
onhover: onhover,
|
||||
onclick: options.tooltip.show ? onclick : null,
|
||||
onzoom: (evt) => {
|
||||
const delta = evt.xMax - evt.xMin;
|
||||
if (delta > 1) {
|
||||
onChangeTimeRange({ from: evt.xMin, to: evt.xMax });
|
||||
}
|
||||
},
|
||||
isToolTipOpen,
|
||||
timeZone,
|
||||
getTimeRange: () => timeRangeRef.current,
|
||||
@ -212,42 +221,71 @@ export const HeatmapPanel = ({
|
||||
);
|
||||
}
|
||||
|
||||
const newVizTooltips = config.featureToggles.newVizTooltips ?? false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VizLayout width={width} height={height} legend={renderLegend()}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart config={builder} data={facets as any} width={vizWidth} height={vizHeight}>
|
||||
{/*children ? children(config, alignedFrame) : null*/}
|
||||
{!newVizTooltips && <ZoomPlugin config={builder} onZoom={onChangeTimeRange} />}
|
||||
{newVizTooltips && options.tooltip.show && (
|
||||
<TooltipPlugin2
|
||||
config={builder}
|
||||
hoverMode={TooltipHoverMode.xyOne}
|
||||
queryZoom={onChangeTimeRange}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
|
||||
return (
|
||||
<HeatmapHoverView
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
dataRef={dataRef}
|
||||
isPinned={isPinned}
|
||||
dismiss={dismiss}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
showColorScale={options.tooltip.showColorScale}
|
||||
canAnnotate={enableAnnotationCreation}
|
||||
panelData={data}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</VizLayout>
|
||||
<Portal>
|
||||
{hover && options.tooltip.show && (
|
||||
<VizTooltipContainer
|
||||
position={{ x: hover.pageX, y: hover.pageY }}
|
||||
offset={{ x: 10, y: 10 }}
|
||||
allowPointerEvents={isToolTipOpen.current}
|
||||
>
|
||||
{shouldDisplayCloseButton && <ExemplarModalHeader onClick={onCloseToolTip} />}
|
||||
<HeatmapHoverView
|
||||
timeRange={timeRange}
|
||||
data={info}
|
||||
hover={hover}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
/>
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</Portal>
|
||||
{!newVizTooltips && (
|
||||
<Portal>
|
||||
{hover && options.tooltip.show && (
|
||||
<VizTooltipContainer
|
||||
position={{ x: hover.pageX, y: hover.pageY }}
|
||||
offset={{ x: 10, y: 10 }}
|
||||
allowPointerEvents={isToolTipOpen.current}
|
||||
>
|
||||
{shouldDisplayCloseButton && <ExemplarModalHeader onClick={onCloseToolTip} />}
|
||||
<HeatmapHoverViewOld
|
||||
timeRange={timeRange}
|
||||
data={info}
|
||||
hover={hover}
|
||||
showHistogram={options.tooltip.yHistogram}
|
||||
replaceVars={replaceVariables}
|
||||
scopedVars={scopedVarsFromRawData}
|
||||
/>
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
colorScaleWrapper: css`
|
||||
margin-left: 25px;
|
||||
padding: 10px 0;
|
||||
max-width: 300px;
|
||||
`,
|
||||
colorScaleWrapper: css({
|
||||
marginLeft: '25px',
|
||||
padding: '10px 0',
|
||||
maxWidth: '300px',
|
||||
}),
|
||||
});
|
||||
|
@ -406,6 +406,14 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
|
||||
showIf: (opts) => opts.tooltip.show,
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
path: 'tooltip.showColorScale',
|
||||
name: 'Show color scale',
|
||||
defaultValue: defaultOptions.tooltip.showColorScale,
|
||||
category,
|
||||
showIf: (opts) => opts.tooltip.show && config.featureToggles.newVizTooltips,
|
||||
});
|
||||
|
||||
category = ['Legend'];
|
||||
builder.addBooleanSwitch({
|
||||
path: 'legend.show',
|
||||
|
@ -82,6 +82,8 @@ composableKinds: PanelCfg: lineage: {
|
||||
show: bool
|
||||
// Controls if the tooltip shows a histogram of the y-axis values
|
||||
yHistogram?: bool
|
||||
// Controls if the tooltip shows a color scale in header
|
||||
showColorScale?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
// Controls legend options
|
||||
HeatmapLegend: {
|
||||
@ -143,8 +145,9 @@ composableKinds: PanelCfg: lineage: {
|
||||
}
|
||||
// Controls tooltip options
|
||||
tooltip: HeatmapTooltip | *{
|
||||
show: true
|
||||
yHistogram: false
|
||||
show: true
|
||||
yHistogram: false
|
||||
showColorScale: false
|
||||
}
|
||||
// Controls exemplar options
|
||||
exemplars: ExemplarConfig | *{
|
||||
|
@ -130,6 +130,10 @@ export interface HeatmapTooltip {
|
||||
* Controls if the tooltip is shown
|
||||
*/
|
||||
show: boolean;
|
||||
/**
|
||||
* Controls if the tooltip shows a color scale in header
|
||||
*/
|
||||
showColorScale?: boolean;
|
||||
/**
|
||||
* Controls if the tooltip shows a histogram of the y-axis values
|
||||
*/
|
||||
@ -261,6 +265,7 @@ export const defaultOptions: Partial<Options> = {
|
||||
tooltip: {
|
||||
show: true,
|
||||
yHistogram: false,
|
||||
showColorScale: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,45 @@
|
||||
import { formatMilliseconds } from './utils';
|
||||
|
||||
describe('heatmap tooltip utils', () => {
|
||||
it('converts ms to appropriate unit', async () => {
|
||||
let msToFormat = 10;
|
||||
let formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('10 milliseconds');
|
||||
|
||||
msToFormat = 1000;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('1 second');
|
||||
|
||||
msToFormat = 1000 * 120;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('2 minutes');
|
||||
|
||||
msToFormat = 1000 * 60 * 60;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('1 hour');
|
||||
|
||||
msToFormat = 1000 * 60 * 60 * 24;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('1 day');
|
||||
|
||||
msToFormat = 1000 * 60 * 60 * 24 * 7 * 3;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('3 weeks');
|
||||
|
||||
msToFormat = 1000 * 60 * 60 * 24 * 7 * 4;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('4 weeks');
|
||||
|
||||
msToFormat = 1000 * 60 * 60 * 24 * 7 * 5;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('1 month');
|
||||
|
||||
msToFormat = 1000 * 60 * 60 * 24 * 365;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('1 year');
|
||||
|
||||
msToFormat = 1000 * 60 * 60 * 24 * 365 * 2;
|
||||
formatted = formatMilliseconds(msToFormat);
|
||||
expect(formatted).toBe('2 years');
|
||||
});
|
||||
});
|
90
public/app/plugins/panel/heatmap/tooltip/utils.ts
Normal file
90
public/app/plugins/panel/heatmap/tooltip/utils.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { DataFrame, Field } from '@grafana/data';
|
||||
|
||||
import { HeatmapData } from '../fields';
|
||||
|
||||
type BucketsMinMax = {
|
||||
xBucketMin: number;
|
||||
xBucketMax: number;
|
||||
yBucketMin: string;
|
||||
yBucketMax: string;
|
||||
};
|
||||
|
||||
export const getHoverCellColor = (data: HeatmapData, index: number) => {
|
||||
const colorPalette = data.heatmapColors?.palette!;
|
||||
const colorIndex = data.heatmapColors?.values[index];
|
||||
|
||||
let cellColor: string | undefined = undefined;
|
||||
|
||||
if (colorIndex != null) {
|
||||
cellColor = colorPalette[colorIndex];
|
||||
}
|
||||
|
||||
return { cellColor, colorPalette };
|
||||
};
|
||||
|
||||
const conversions: Record<string, number> = {
|
||||
year: 1000 * 60 * 60 * 24 * 365,
|
||||
month: 1000 * 60 * 60 * 24 * 30,
|
||||
week: 1000 * 60 * 60 * 24 * 7,
|
||||
day: 1000 * 60 * 60 * 24,
|
||||
hour: 1000 * 60 * 60,
|
||||
minute: 1000 * 60,
|
||||
second: 1000,
|
||||
millisecond: 1,
|
||||
};
|
||||
|
||||
// @TODO: display "~ 1 year/month"?
|
||||
export const formatMilliseconds = (milliseconds: number) => {
|
||||
let value = 1;
|
||||
let unit = 'millisecond';
|
||||
|
||||
for (unit in conversions) {
|
||||
if (milliseconds >= conversions[unit]) {
|
||||
value = Math.floor(milliseconds / conversions[unit]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const unitString = value === 1 ? unit : unit + 's';
|
||||
|
||||
return `${value} ${unitString}`;
|
||||
};
|
||||
|
||||
export const getFieldFromData = (data: DataFrame, fieldType: string, isSparse: boolean) => {
|
||||
let field: Field | undefined;
|
||||
|
||||
switch (fieldType) {
|
||||
case 'x':
|
||||
field = isSparse
|
||||
? data?.fields.find(({ name }) => name === 'x' || name === 'xMin' || name === 'xMax')
|
||||
: data?.fields[0];
|
||||
break;
|
||||
case 'y':
|
||||
field = isSparse
|
||||
? data?.fields.find(({ name }) => name === 'y' || name === 'yMin' || name === 'yMax')
|
||||
: data?.fields[1];
|
||||
break;
|
||||
case 'count':
|
||||
field = isSparse ? data?.fields.find(({ name }) => name === 'count') : data?.fields[2];
|
||||
break;
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
export const getSparseCellMinMax = (data: HeatmapData, index: number): BucketsMinMax => {
|
||||
let fields = data.heatmap!.fields;
|
||||
|
||||
let xMax = fields.find((f) => f.name === 'xMax')!;
|
||||
let yMin = fields.find((f) => f.name === 'yMin')!;
|
||||
let yMax = fields.find((f) => f.name === 'yMax')!;
|
||||
|
||||
let interval = xMax.config.interval!;
|
||||
|
||||
return {
|
||||
xBucketMin: xMax.values[index] - interval,
|
||||
xBucketMax: xMax.values[index],
|
||||
yBucketMin: yMin.values[index],
|
||||
yBucketMax: yMax.values[index],
|
||||
};
|
||||
};
|
@ -64,7 +64,7 @@ interface PrepConfigOpts {
|
||||
onhover?: null | ((evt?: HeatmapHoverEvent | null) => void);
|
||||
onclick?: null | ((evt?: Object) => void);
|
||||
onzoom?: null | ((evt: HeatmapZoomEvent) => void);
|
||||
isToolTipOpen: MutableRefObject<boolean>;
|
||||
isToolTipOpen?: MutableRefObject<boolean>;
|
||||
timeZone: string;
|
||||
getTimeRange: () => TimeRange;
|
||||
exemplarColor: string;
|
||||
@ -85,7 +85,6 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
eventBus,
|
||||
onhover,
|
||||
onclick,
|
||||
onzoom,
|
||||
isToolTipOpen,
|
||||
timeZone,
|
||||
getTimeRange,
|
||||
@ -143,15 +142,6 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
);
|
||||
});
|
||||
|
||||
onzoom &&
|
||||
builder.addHook('setSelect', (u) => {
|
||||
onzoom({
|
||||
xMin: u.posToVal(u.select.left, xScaleKey),
|
||||
xMax: u.posToVal(u.select.left + u.select.width, xScaleKey),
|
||||
});
|
||||
u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
|
||||
});
|
||||
|
||||
if (isTime) {
|
||||
// this is a tmp hack because in mode: 2, uplot does not currently call scales.x.range() for setData() calls
|
||||
// scales.x.range() typically reads back from drilled-down panelProps.timeRange via getTimeRange()
|
||||
@ -197,7 +187,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
payload.point[xScaleUnit] = u.posToVal(left!, xScaleKey);
|
||||
eventBus.publish(hoverEvent);
|
||||
|
||||
if (!isToolTipOpen.current) {
|
||||
if (!isToolTipOpen?.current) {
|
||||
if (pendingOnleave) {
|
||||
clearTimeout(pendingOnleave);
|
||||
pendingOnleave = 0;
|
||||
@ -214,7 +204,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isToolTipOpen.current) {
|
||||
if (!isToolTipOpen?.current) {
|
||||
// if tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms)
|
||||
if (!pendingOnleave) {
|
||||
pendingOnleave = setTimeout(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user