1
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

228 lines
7.0 KiB

import React, { useMemo, useRef } from 'react';
import { TooltipDisplayMode, StackingMode, LegendDisplayMode } from '@grafana/schema';
import {
compareDataFrameStructures,
DataFrame,
getFieldDisplayName,
PanelProps,
TimeRange,
VizOrientation,
} from '@grafana/data';
import {
GraphNG,
GraphNGProps,
measureText,
PlotLegend,
TooltipPlugin,
UPlotConfigBuilder,
UPLOT_AXIS_FONT_SIZE,
usePanelContext,
useTheme2,
VizLayout,
VizLegend,
} from '@grafana/ui';
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';
/**
* @alpha
*/
export interface BarChartProps
extends PanelOptions,
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend' | 'theme'> {}
const propsToDiff: Array<string | PropDiffFn> = [
'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<PanelOptions> {}
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone, id }) => {
const theme = useTheme2();
const { eventBus } = usePanelContext();
const frame0Ref = useRef<DataFrame>();
const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]);
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 = info.viz;
const f1 = frame0Ref.current;
if (!(f0 && f1 && compareDataFrameStructures(f0, f1, true))) {
structureRef.current++;
}
frame0Ref.current = f0;
return (data.structureRev ?? 0) + structureRef.current;
}, [info, 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 aproximation.
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]);
// 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 <PanelDataErrorView panelId={id} data={data} message={info.warn} needsNumberField={true} />;
}
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 <DataHoverView data={info.aligned} rowIndex={datapointIdx} columnIndex={seriesIdx} />;
};
const renderLegend = (config: UPlotConfigBuilder) => {
const { legend } = options;
if (!config || legend.displayMode === LegendDisplayMode.Hidden) {
return null;
}
if (info.colorByField) {
const items = getFieldLegendItem([info.colorByField], theme);
if (items?.length) {
return (
<VizLayout.Legend placement={legend.placement}>
<VizLegend placement={legend.placement} items={items} displayMode={legend.displayMode} />
</VizLayout.Legend>
);
}
}
return <PlotLegend data={[info.viz]} config={config} maxHeight="35%" maxWidth="60%" {...options.legend} />;
};
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(colorByField.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,
timeZone,
eventBus,
orientation,
barWidth,
barRadius,
showValue,
groupWidth,
xTickLabelRotation,
xTickLabelMaxLength,
xTickLabelSpacing,
stacking,
legend,
tooltip,
text,
rawValue,
getColor,
fillOpacity,
allFrames: [info.viz],
});
};
return (
<GraphNG
theme={theme}
frames={[info.viz]}
prepConfig={prepConfig}
propsToDiff={propsToDiff}
preparePlotFrame={(f) => 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, alignedFrame) => {
return (
<TooltipPlugin
data={alignedFrame}
config={config}
mode={tooltip.mode}
timeZone={timeZone}
renderTooltip={renderTooltip}
/>
);
}}
</GraphNG>
);
};