diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 3b1e6cdb9ac..267c5f49b97 100755 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -1,9 +1,12 @@ -import React, { useMemo, useRef } from 'react'; -import { TooltipDisplayMode, StackingMode, LegendDisplayMode } from '@grafana/schema'; +import React, { useMemo, useRef, useState } from 'react'; +import { css } from '@emotion/css'; +import { LegendDisplayMode } from '@grafana/schema'; import { + CartesianCoords2D, compareDataFrameStructures, DataFrame, getFieldDisplayName, + GrafanaTheme2, PanelProps, TimeRange, VizOrientation, @@ -13,20 +16,27 @@ import { GraphNGProps, measureText, PlotLegend, - TooltipPlugin, + Portal, UPlotConfigBuilder, UPLOT_AXIS_FONT_SIZE, usePanelContext, + useStyles2, useTheme2, VizLayout, VizLegend, + VizTooltipContainer, } from '@grafana/ui'; +import { PanelDataErrorView } from '@grafana/runtime'; +import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG'; + import { PanelOptions } from './models.gen'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; -import { PanelDataErrorView } from '@grafana/runtime'; import { DataHoverView } from '../geomap/components/DataHoverView'; 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 @@ -55,8 +65,31 @@ interface Props extends PanelProps {} export const BarChartPanel: React.FunctionComponent = ({ data, options, width, height, timeZone, id }) => { const theme = useTheme2(); + const styles = useStyles2(getStyles); const { eventBus } = usePanelContext(); + const oldConfig = useRef(undefined); + const isToolTipOpen = useRef(false); + + const [hover, setHover] = useState(undefined); + const [coords, setCoords] = useState(null); + const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); + const [focusedPointIdx, setFocusedPointIdx] = useState(null); + const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(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(); const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]); const structureRef = useRef(10000); @@ -86,7 +119,7 @@ export const BarChartPanel: React.FunctionComponent = ({ 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 (!options.xTickLabelMaxLength) { 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; return ( @@ -99,14 +132,6 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w } }, [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) { return ; } @@ -119,12 +144,20 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w } return ( - + <> + {shouldDisplayCloseButton && ( + <> + +
+ + )} + + ); }; @@ -220,16 +253,38 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w height={height} > {(config, alignedFrame) => { + if (oldConfig.current !== config) { + oldConfig.current = setupConfig({ + config, + onUPlotClick, + setFocusedSeriesIdx, + setFocusedPointIdx, + setCoords, + setHover, + isToolTipOpen, + }); + } + return ( - + + {hover && coords && ( + + {renderTooltip(info.aligned, focusedSeriesIdx, focusedPointIdx)} + + )} + ); }} ); }; + +const getStyles = (theme: GrafanaTheme2) => ({ + closeButtonSpacer: css` + margin-bottom: 15px; + `, +}); diff --git a/public/app/plugins/panel/barchart/config.ts b/public/app/plugins/panel/barchart/config.ts new file mode 100644 index 00000000000..3cd94e6745d --- /dev/null +++ b/public/app/plugins/panel/barchart/config.ts @@ -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>; + setFocusedPointIdx: Dispatch>; + setCoords: Dispatch>; + setHover: Dispatch>; + isToolTipOpen: MutableRefObject; +}; + +// 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; +}; diff --git a/public/app/plugins/panel/geomap/GeomapTooltip.tsx b/public/app/plugins/panel/geomap/GeomapTooltip.tsx index 50703989299..6dd9578e2d5 100644 --- a/public/app/plugins/panel/geomap/GeomapTooltip.tsx +++ b/public/app/plugins/panel/geomap/GeomapTooltip.tsx @@ -22,7 +22,7 @@ export const GeomapTooltip = ({ ttip, onClose, isOpen }: Props) => { {ttip && ttip.layers && (
- +
)}