Tooltip: Add tooltip support to Histogram (#89196)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2024-06-14 10:23:25 -06:00 committed by GitHub
parent f5468542ba
commit c3b772618a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 142 additions and 16 deletions

View File

@ -341,7 +341,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"align": "auto",
@ -436,7 +436,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"align": "auto",
@ -523,7 +523,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 20,
@ -608,7 +608,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 20,
@ -693,7 +693,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 20,
@ -778,7 +778,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
@ -845,7 +845,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,

View File

@ -28,7 +28,7 @@ export const VizTooltipContent = ({
const scrollableStyle: CSSProperties = scrollable
? {
maxHeight: maxHeight,
overflowY: 'scroll',
overflowY: 'auto',
}
: {};

View File

@ -47,7 +47,7 @@ export const VizTooltipRow = ({
maxHeight: 55,
whiteSpace: 'wrap',
wordBreak: 'break-word',
overflowY: 'scroll',
overflowY: 'auto',
}
: {
whiteSpace: 'wrap',

View File

@ -46,7 +46,7 @@ export interface HistogramProps extends Themeable2 {
structureRev?: number; // a number that will change when the frames[] structure changes
legend: VizLegendOptions;
rawSeries?: DataFrame[];
children?: (builder: UPlotConfigBuilder, frame: DataFrame) => React.ReactNode;
children?: (builder: UPlotConfigBuilder, frame: DataFrame, xMinOnlyFrame: DataFrame) => React.ReactNode;
}
export function getBucketSize(frame: DataFrame) {
@ -283,6 +283,7 @@ interface State {
alignedData: AlignedData;
alignedFrame: DataFrame;
config?: UPlotConfigBuilder;
xMinOnlyFrame: DataFrame;
}
export class Histogram extends React.Component<HistogramProps, State> {
@ -295,12 +296,14 @@ export class Histogram extends React.Component<HistogramProps, State> {
const { alignedFrame } = props;
const config = withConfig ? prepConfig(alignedFrame, this.props.theme) : this.state.config!;
const alignedData = preparePlotData(config, xMinOnlyFrame(alignedFrame));
const xMinOnly = xMinOnlyFrame(alignedFrame);
const alignedData = preparePlotData(config, xMinOnly);
return {
alignedFrame,
alignedData,
config,
xMinOnlyFrame: xMinOnly,
};
}
@ -347,7 +350,7 @@ export class Histogram extends React.Component<HistogramProps, State> {
<VizLayout width={width} height={height} legend={this.renderLegend(config)}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart config={this.state.config!} data={this.state.alignedData} width={vizWidth} height={vizHeight}>
{children ? children(config, alignedFrame) : null}
{children ? children(config, alignedFrame, this.state.xMinOnlyFrame) : null}
</UPlotChart>
)}
</VizLayout>

View File

@ -2,9 +2,11 @@ import React, { useMemo } from 'react';
import { PanelProps, buildHistogram, getHistogramFields } from '@grafana/data';
import { histogramFieldsToFrame } from '@grafana/data/src/transformations/transformers/histogram';
import { useTheme2 } from '@grafana/ui';
import { TooltipDisplayMode, TooltipPlugin2, useTheme2 } from '@grafana/ui';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { Histogram, getBucketSize } from './Histogram';
import { HistogramTooltip } from './HistogramTooltip';
import { Options } from './panelcfg.gen';
type Props = PanelProps<Options>;
@ -67,8 +69,34 @@ export const HistogramPanel = ({ data, options, width, height }: Props) => {
bucketSize={bucketSize}
bucketCount={options.bucketCount}
>
{(config, alignedFrame) => {
return null; // <TooltipPlugin data={alignedFrame} config={config} mode={options.tooltip.mode} timeZone={timeZone} />;
{(builder, alignedFrame, xMinOnlyFrame) => {
return (
<>
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
}
render={(u, dataIdxs, seriesIdx, isPinned = false) => {
return (
<HistogramTooltip
series={histogram}
xMinOnlyFrame={xMinOnlyFrame}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
maxHeight={options.tooltip.maxHeight}
/>
);
}}
maxWidth={options.tooltip.maxWidth}
/>
)}
</>
);
}}
</Histogram>
);

View File

@ -0,0 +1,95 @@
import { css } from '@emotion/css';
import React, { ReactNode, useMemo } from 'react';
import { DataFrame, formattedValueToString } from '@grafana/data';
import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen';
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 { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types';
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils';
import { getDataLinks } from '../status-history/utils';
import { isTooltipScrollable } from '../timeseries/utils';
export interface HistogramTooltipProps {
// aligned series frame
series: DataFrame;
xMinOnlyFrame: DataFrame;
// hovered points
dataIdxs: Array<number | null>;
// closest/hovered series
seriesIdx?: number | null;
mode?: TooltipDisplayMode;
sortOrder?: SortOrder;
isPinned: boolean;
maxHeight?: number;
}
export const HistogramTooltip = ({
series,
xMinOnlyFrame,
dataIdxs,
seriesIdx,
mode = TooltipDisplayMode.Single,
sortOrder = SortOrder.None,
isPinned,
maxHeight,
}: HistogramTooltipProps) => {
const styles = useStyles2(getStyles);
const xMinField = series.fields[0];
const xMaxField = series.fields[1];
// use the formatter from other bucket bound if none is defined
const { display: xMinDisp } = xMinField.config.unit != null ? xMinField : xMaxField;
const { display: xMaxDisp } = xMaxField.config.unit != null ? xMaxField : xMinField;
const xMinVal = formattedValueToString(xMinDisp!(xMinField.values[dataIdxs[0]!]));
const xMaxVal = formattedValueToString(xMaxDisp!(xMaxField.values[dataIdxs[1]!]));
const headerItem: VizTooltipItem | null = xMinField.config.custom?.hideFrom?.tooltip
? null
: {
label: 'Bucket',
value: `${xMinVal} - ${xMaxVal}`,
};
const contentItems = useMemo(
() => getContentItems(xMinOnlyFrame.fields, xMinField, dataIdxs, seriesIdx, mode, sortOrder),
[xMinOnlyFrame.fields, xMinField, dataIdxs, seriesIdx, mode, sortOrder]
);
let footer: ReactNode;
if (isPinned && seriesIdx != null) {
const field = series.fields[seriesIdx];
const dataIdx = dataIdxs[seriesIdx]!;
const links = getDataLinks(field, dataIdx);
footer = <VizTooltipFooter dataLinks={links} />;
}
return (
<div className={styles.wrapper}>
{headerItem != null && <VizTooltipHeader item={headerItem} isPinned={isPinned} />}
<VizTooltipContent
items={contentItems}
isPinned={isPinned}
scrollable={isTooltipScrollable({ mode, maxHeight })}
maxHeight={maxHeight}
/>
{footer}
</div>
);
};
export const getStyles = () => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
}),
});

View File

@ -66,7 +66,7 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(HistogramPanel)
showIf: (opts, data) => !originalDataHasHistogram(data),
});
// commonOptionsBuilder.addTooltipOptions(builder);
commonOptionsBuilder.addTooltipOptions(builder);
commonOptionsBuilder.addLegendOptions(builder);
})
.useFieldConfig({