mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: Add support for data links (#44932)
This commit is contained in:
parent
b07345e57e
commit
3a2e3267ba
@ -1,9 +1,12 @@
|
|||||||
import React, { useMemo, useRef } from 'react';
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
import { TooltipDisplayMode, StackingMode, LegendDisplayMode } from '@grafana/schema';
|
import { css } from '@emotion/css';
|
||||||
|
import { LegendDisplayMode } from '@grafana/schema';
|
||||||
import {
|
import {
|
||||||
|
CartesianCoords2D,
|
||||||
compareDataFrameStructures,
|
compareDataFrameStructures,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
|
GrafanaTheme2,
|
||||||
PanelProps,
|
PanelProps,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
VizOrientation,
|
VizOrientation,
|
||||||
@ -13,20 +16,27 @@ import {
|
|||||||
GraphNGProps,
|
GraphNGProps,
|
||||||
measureText,
|
measureText,
|
||||||
PlotLegend,
|
PlotLegend,
|
||||||
TooltipPlugin,
|
Portal,
|
||||||
UPlotConfigBuilder,
|
UPlotConfigBuilder,
|
||||||
UPLOT_AXIS_FONT_SIZE,
|
UPLOT_AXIS_FONT_SIZE,
|
||||||
usePanelContext,
|
usePanelContext,
|
||||||
|
useStyles2,
|
||||||
useTheme2,
|
useTheme2,
|
||||||
VizLayout,
|
VizLayout,
|
||||||
VizLegend,
|
VizLegend,
|
||||||
|
VizTooltipContainer,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { PanelDataErrorView } from '@grafana/runtime';
|
||||||
|
import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG';
|
||||||
|
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
|
import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
|
||||||
import { PanelDataErrorView } from '@grafana/runtime';
|
|
||||||
import { DataHoverView } from '../geomap/components/DataHoverView';
|
import { DataHoverView } from '../geomap/components/DataHoverView';
|
||||||
import { getFieldLegendItem } from '../state-timeline/utils';
|
import { getFieldLegendItem } from '../state-timeline/utils';
|
||||||
import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG';
|
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||||
|
import { HoverEvent, setupConfig } from './config';
|
||||||
|
|
||||||
|
const TOOLTIP_OFFSET = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
@ -55,8 +65,31 @@ interface Props extends PanelProps<PanelOptions> {}
|
|||||||
|
|
||||||
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone, id }) => {
|
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone, id }) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
const { eventBus } = usePanelContext();
|
const { eventBus } = usePanelContext();
|
||||||
|
|
||||||
|
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
|
||||||
|
const isToolTipOpen = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const [hover, setHover] = useState<HoverEvent | undefined>(undefined);
|
||||||
|
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
||||||
|
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
||||||
|
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
||||||
|
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onCloseToolTip = () => {
|
||||||
|
isToolTipOpen.current = false;
|
||||||
|
setCoords(null);
|
||||||
|
setShouldDisplayCloseButton(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUPlotClick = () => {
|
||||||
|
isToolTipOpen.current = !isToolTipOpen.current;
|
||||||
|
|
||||||
|
// Linking into useState required to re-render tooltip
|
||||||
|
setShouldDisplayCloseButton(isToolTipOpen.current);
|
||||||
|
};
|
||||||
|
|
||||||
const frame0Ref = useRef<DataFrame>();
|
const frame0Ref = useRef<DataFrame>();
|
||||||
const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]);
|
const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]);
|
||||||
const structureRef = useRef(10000);
|
const structureRef = useRef(10000);
|
||||||
@ -86,7 +119,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
// If no max length is set, limit the number of characters to a length where it will use a maximum of half of the height of the viz.
|
// If no max length is set, limit the number of characters to a length where it will use a maximum of half of the height of the viz.
|
||||||
if (!options.xTickLabelMaxLength) {
|
if (!options.xTickLabelMaxLength) {
|
||||||
const rotationAngle = options.xTickLabelRotation;
|
const rotationAngle = options.xTickLabelRotation;
|
||||||
const textSize = measureText('M', UPLOT_AXIS_FONT_SIZE).width; // M is usually the widest character so let's use that as an aproximation.
|
const textSize = measureText('M', UPLOT_AXIS_FONT_SIZE).width; // M is usually the widest character so let's use that as an approximation.
|
||||||
const maxHeightForValues = height / 2;
|
const maxHeightForValues = height / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -99,14 +132,6 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
}
|
}
|
||||||
}, [height, options.xTickLabelRotation, options.xTickLabelMaxLength]);
|
}, [height, options.xTickLabelRotation, options.xTickLabelMaxLength]);
|
||||||
|
|
||||||
// Force 'multi' tooltip setting or stacking mode
|
|
||||||
const tooltip = useMemo(() => {
|
|
||||||
if (options.stacking === StackingMode.Normal || options.stacking === StackingMode.Percent) {
|
|
||||||
return { ...options.tooltip, mode: TooltipDisplayMode.Multi };
|
|
||||||
}
|
|
||||||
return options.tooltip;
|
|
||||||
}, [options.tooltip, options.stacking]);
|
|
||||||
|
|
||||||
if (!info.viz?.fields.length) {
|
if (!info.viz?.fields.length) {
|
||||||
return <PanelDataErrorView panelId={id} data={data} message={info.warn} needsNumberField={true} />;
|
return <PanelDataErrorView panelId={id} data={data} message={info.warn} needsNumberField={true} />;
|
||||||
}
|
}
|
||||||
@ -119,12 +144,20 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataHoverView
|
<>
|
||||||
data={info.aligned}
|
{shouldDisplayCloseButton && (
|
||||||
rowIndex={datapointIdx}
|
<>
|
||||||
columnIndex={seriesIdx}
|
<CloseButton onClick={onCloseToolTip} />
|
||||||
sortOrder={options.tooltip.sort}
|
<div className={styles.closeButtonSpacer} />
|
||||||
/>
|
</>
|
||||||
|
)}
|
||||||
|
<DataHoverView
|
||||||
|
data={info.aligned}
|
||||||
|
rowIndex={datapointIdx}
|
||||||
|
columnIndex={seriesIdx}
|
||||||
|
sortOrder={options.tooltip.sort}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,16 +253,38 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
height={height}
|
height={height}
|
||||||
>
|
>
|
||||||
{(config, alignedFrame) => {
|
{(config, alignedFrame) => {
|
||||||
|
if (oldConfig.current !== config) {
|
||||||
|
oldConfig.current = setupConfig({
|
||||||
|
config,
|
||||||
|
onUPlotClick,
|
||||||
|
setFocusedSeriesIdx,
|
||||||
|
setFocusedPointIdx,
|
||||||
|
setCoords,
|
||||||
|
setHover,
|
||||||
|
isToolTipOpen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipPlugin
|
<Portal>
|
||||||
data={alignedFrame}
|
{hover && coords && (
|
||||||
config={config}
|
<VizTooltipContainer
|
||||||
mode={tooltip.mode}
|
position={{ x: coords.x, y: coords.y }}
|
||||||
timeZone={timeZone}
|
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
|
||||||
renderTooltip={renderTooltip}
|
allowPointerEvents
|
||||||
/>
|
>
|
||||||
|
{renderTooltip(info.aligned, focusedSeriesIdx, focusedPointIdx)}
|
||||||
|
</VizTooltipContainer>
|
||||||
|
)}
|
||||||
|
</Portal>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</GraphNG>
|
</GraphNG>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
closeButtonSpacer: css`
|
||||||
|
margin-bottom: 15px;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
102
public/app/plugins/panel/barchart/config.ts
Normal file
102
public/app/plugins/panel/barchart/config.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
|
import { UPlotConfigBuilder } from '@grafana/ui';
|
||||||
|
import { positionTooltip } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin';
|
||||||
|
import { CartesianCoords2D } from '@grafana/data';
|
||||||
|
|
||||||
|
export type HoverEvent = {
|
||||||
|
xIndex: number;
|
||||||
|
yIndex: number;
|
||||||
|
pageX: number;
|
||||||
|
pageY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetupConfigParams = {
|
||||||
|
config: UPlotConfigBuilder;
|
||||||
|
onUPlotClick: () => void;
|
||||||
|
setFocusedSeriesIdx: Dispatch<SetStateAction<number | null>>;
|
||||||
|
setFocusedPointIdx: Dispatch<SetStateAction<number | null>>;
|
||||||
|
setCoords: Dispatch<SetStateAction<CartesianCoords2D | null>>;
|
||||||
|
setHover: Dispatch<SetStateAction<HoverEvent | undefined>>;
|
||||||
|
isToolTipOpen: MutableRefObject<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This applies config hooks to setup tooltip listener. Ideally this could happen in the same `prepConfig` function
|
||||||
|
// however the GraphNG structures do not allow access to the `setHover` callback
|
||||||
|
export const setupConfig = ({
|
||||||
|
config,
|
||||||
|
onUPlotClick,
|
||||||
|
setFocusedSeriesIdx,
|
||||||
|
setFocusedPointIdx,
|
||||||
|
setCoords,
|
||||||
|
setHover,
|
||||||
|
isToolTipOpen,
|
||||||
|
}: SetupConfigParams): UPlotConfigBuilder => {
|
||||||
|
config.addHook('init', (u) => {
|
||||||
|
u.root.parentElement?.addEventListener('click', onUPlotClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
let rect: DOMRect;
|
||||||
|
// rect of .u-over (grid area)
|
||||||
|
config.addHook('syncRect', (u, r) => {
|
||||||
|
rect = r;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipInterpolator = config.getTooltipInterpolator();
|
||||||
|
if (tooltipInterpolator) {
|
||||||
|
config.addHook('setCursor', (u) => {
|
||||||
|
tooltipInterpolator(
|
||||||
|
setFocusedSeriesIdx,
|
||||||
|
setFocusedPointIdx,
|
||||||
|
(clear) => {
|
||||||
|
if (clear && !isToolTipOpen.current) {
|
||||||
|
setCoords(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = positionTooltip(u, rect);
|
||||||
|
if (x !== undefined && y !== undefined && !isToolTipOpen.current) {
|
||||||
|
setCoords({ x, y });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
u
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config.addHook('setLegend', (u) => {
|
||||||
|
if (!isToolTipOpen.current) {
|
||||||
|
setFocusedPointIdx(u.legend.idx!);
|
||||||
|
}
|
||||||
|
if (u.cursor.idxs != null) {
|
||||||
|
for (let i = 0; i < u.cursor.idxs.length; i++) {
|
||||||
|
const sel = u.cursor.idxs[i];
|
||||||
|
if (sel != null) {
|
||||||
|
const hover: HoverEvent = {
|
||||||
|
xIndex: sel,
|
||||||
|
yIndex: 0,
|
||||||
|
pageX: rect.left + u.cursor.left!,
|
||||||
|
pageY: rect.top + u.cursor.top!,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isToolTipOpen.current || !hover) {
|
||||||
|
setHover(hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // only show the first one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
config.addHook('setSeries', (_, idx) => {
|
||||||
|
if (!isToolTipOpen.current) {
|
||||||
|
setFocusedSeriesIdx(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
@ -22,7 +22,7 @@ export const GeomapTooltip = ({ ttip, onClose, isOpen }: Props) => {
|
|||||||
{ttip && ttip.layers && (
|
{ttip && ttip.layers && (
|
||||||
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }} allowPointerEvents>
|
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }} allowPointerEvents>
|
||||||
<section ref={ref} {...overlayProps} {...dialogProps}>
|
<section ref={ref} {...overlayProps} {...dialogProps}>
|
||||||
<ComplexDataHoverView {...ttip} isOpen={isOpen} onClose={onClose} />
|
<ComplexDataHoverView layers={ttip.layers} isOpen={isOpen} onClose={onClose} />
|
||||||
</section>
|
</section>
|
||||||
</VizTooltipContainer>
|
</VizTooltipContainer>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user