import React, { useMemo, useRef, useState } from 'react'; import { CartesianCoords2D, compareDataFrameStructures, DataFrame, Field, getFieldDisplayName, PanelProps, TimeRange, VizOrientation, } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { GraphNG, GraphNGProps, measureText, PlotLegend, Portal, TooltipDisplayMode, UPlotConfigBuilder, UPLOT_AXIS_FONT_SIZE, usePanelContext, useTheme2, VizLayout, VizLegend, VizTooltipContainer, } from '@grafana/ui'; import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { DataHoverView } from '../geomap/components/DataHoverView'; import { getFieldLegendItem } from '../state-timeline/utils'; import { PanelOptions } from './models.gen'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; const TOOLTIP_OFFSET = 10; /** * @alpha */ export interface BarChartProps extends PanelOptions, Omit {} const propsToDiff: Array = [ 'orientation', 'barWidth', 'barRadius', 'xTickLabelRotation', 'xTickLabelMaxLength', 'xTickLabelSpacing', 'groupWidth', 'stacking', 'showValue', 'xField', 'colorField', 'legend', (prev: BarChartProps, next: BarChartProps) => next.text?.valueSize === prev.text?.valueSize, ]; interface Props extends PanelProps {} export const BarChartPanel: React.FunctionComponent = ({ data, options, fieldConfig, width, height, timeZone, id, }) => { const theme = useTheme2(); const { eventBus } = usePanelContext(); const oldConfig = useRef(undefined); const isToolTipOpen = useRef(false); const [hover, setHover] = useState(undefined); const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(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 colorByFieldRef = useRef(); const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]); const chartDisplay = 'viz' in info ? info : null; colorByFieldRef.current = chartDisplay?.colorByField; const structureRef = useRef(10000); useMemo(() => { structureRef.current++; // eslint-disable-next-line react-hooks/exhaustive-deps }, [options]); // change every time the options object changes (while editing) const structureRev = useMemo(() => { const f0 = chartDisplay?.viz[0]; const f1 = frame0Ref.current; if (!(f0 && f1 && compareDataFrameStructures(f0, f1, true))) { structureRef.current++; } frame0Ref.current = f0; return (data.structureRev ?? 0) + structureRef.current; }, [chartDisplay, data.structureRev]); const orientation = useMemo(() => { if (!options.orientation || options.orientation === VizOrientation.Auto) { return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical; } return options.orientation; }, [width, height, options.orientation]); const xTickLabelMaxLength = useMemo(() => { // 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 approximation. const maxHeightForValues = height / 2; return ( maxHeightForValues / (Math.sin(((rotationAngle >= 0 ? rotationAngle : rotationAngle * -1) * Math.PI) / 180) * textSize) - 3 //Subtract 3 for the "..." added to the end. ); } else { return options.xTickLabelMaxLength; } }, [height, options.xTickLabelRotation, options.xTickLabelMaxLength]); if ('warn' in info) { return ( ); } const renderTooltip = (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => { const field = seriesIdx == null ? null : alignedFrame.fields[seriesIdx]; if (field) { const disp = getFieldDisplayName(field, alignedFrame); seriesIdx = info.aligned.fields.findIndex((f) => disp === getFieldDisplayName(f, info.aligned)); } return ( <> {shouldDisplayCloseButton && (
)} ); }; const renderLegend = (config: UPlotConfigBuilder) => { const { legend } = options; if (!config || legend.showLegend === false) { return null; } if (info.colorByField) { const items = getFieldLegendItem([info.colorByField], theme); if (items?.length) { return ( ); } } return ; }; const rawValue = (seriesIdx: number, valueIdx: number) => { return frame0Ref.current!.fields[seriesIdx].values.get(valueIdx); }; // Color by value let getColor: ((seriesIdx: number, valueIdx: number) => string) | undefined = undefined; let fillOpacity = 1; if (info.colorByField) { const colorByField = info.colorByField; const disp = colorByField.display!; fillOpacity = (colorByField.config.custom.fillOpacity ?? 100) / 100; // gradientMode? ignore? getColor = (seriesIdx: number, valueIdx: number) => disp(colorByFieldRef.current?.values.get(valueIdx)).color!; } const prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { const { barWidth, barRadius = 0, showValue, groupWidth, stacking, legend, tooltip, text, xTickLabelRotation, xTickLabelSpacing, } = options; return preparePlotConfigBuilder({ frame: alignedFrame, getTimeRange, theme, timeZones: [timeZone], eventBus, orientation, barWidth, barRadius, showValue, groupWidth, xTickLabelRotation, xTickLabelMaxLength, xTickLabelSpacing, stacking, legend, tooltip, text, rawValue, getColor, fillOpacity, allFrames: info.viz, }); }; return ( f[0]} // already processed in by the panel above! renderLegend={renderLegend} legend={options.legend} timeZone={timeZone} timeRange={{ from: 1, to: 1 } as unknown as TimeRange} // HACK structureRev={structureRev} width={width} height={height} > {(config) => { if (oldConfig.current !== config) { oldConfig.current = addTooltipSupport({ config, onUPlotClick, setFocusedSeriesIdx, setFocusedPointIdx, setCoords, setHover, isToolTipOpen, }); } if (options.tooltip.mode === TooltipDisplayMode.None) { return null; } return ( {hover && coords && ( {renderTooltip(info.aligned, focusedSeriesIdx, focusedPointIdx)} )} ); }} ); };