mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: color by field, x time field, bar radius, label skipping (#43257)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
4233a62aeb
commit
e75edc810d
@ -185,7 +185,13 @@ const timeUnitSize = {
|
||||
};
|
||||
|
||||
/** Format time axis ticks */
|
||||
function formatTime(self: uPlot, splits: number[], axisIdx: number, foundSpace: number, foundIncr: number): string[] {
|
||||
export function formatTime(
|
||||
self: uPlot,
|
||||
splits: number[],
|
||||
axisIdx: number,
|
||||
foundSpace: number,
|
||||
foundIncr: number
|
||||
): string[] {
|
||||
const timeZone = (self.axes[axisIdx] as any).timeZone;
|
||||
const scale = self.scales.x;
|
||||
const range = (scale?.max ?? 0) - (scale?.min ?? 0);
|
||||
|
@ -1,103 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { DataFrame, FieldType, TimeRange } from '@grafana/data';
|
||||
import { GraphNG, GraphNGProps, PlotLegend, UPlotConfigBuilder, usePanelContext, useTheme2 } from '@grafana/ui';
|
||||
import { LegendDisplayMode } from '@grafana/schema';
|
||||
import { BarChartOptions } from './types';
|
||||
import { isLegendOrdered, preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||
import { PropDiffFn } from '../../../../../packages/grafana-ui/src/components/GraphNG/GraphNG';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface BarChartProps
|
||||
extends BarChartOptions,
|
||||
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend' | 'theme'> {}
|
||||
|
||||
const propsToDiff: Array<string | PropDiffFn> = [
|
||||
'orientation',
|
||||
'barWidth',
|
||||
'xTickLabelRotation',
|
||||
'xTickLabelMaxLength',
|
||||
'groupWidth',
|
||||
'stacking',
|
||||
'showValue',
|
||||
'legend',
|
||||
(prev: BarChartProps, next: BarChartProps) => next.text?.valueSize === prev.text?.valueSize,
|
||||
];
|
||||
|
||||
export const BarChart: React.FC<BarChartProps> = (props) => {
|
||||
const theme = useTheme2();
|
||||
const { eventBus } = usePanelContext();
|
||||
|
||||
const frame0Ref = useRef<DataFrame>();
|
||||
frame0Ref.current = props.frames[0];
|
||||
|
||||
const renderLegend = (config: UPlotConfigBuilder) => {
|
||||
if (!config || props.legend.displayMode === LegendDisplayMode.Hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PlotLegend data={props.frames} config={config} maxHeight="35%" maxWidth="60%" {...props.legend} />;
|
||||
};
|
||||
|
||||
const rawValue = (seriesIdx: number, valueIdx: number) => {
|
||||
// When sorted by legend state.seriesIndex is not changed and is not equal to the sorted index of the field
|
||||
if (isLegendOrdered(props.legend)) {
|
||||
return frame0Ref.current!.fields[seriesIdx].values.get(valueIdx);
|
||||
}
|
||||
|
||||
let field = frame0Ref.current!.fields.find(
|
||||
(f) => f.type === FieldType.number && f.state?.seriesIndex === seriesIdx - 1
|
||||
);
|
||||
return field!.values.get(valueIdx);
|
||||
};
|
||||
|
||||
const prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const {
|
||||
timeZone,
|
||||
orientation,
|
||||
barWidth,
|
||||
showValue,
|
||||
groupWidth,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text,
|
||||
xTickLabelRotation,
|
||||
xTickLabelMaxLength,
|
||||
} = props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
getTimeRange,
|
||||
theme,
|
||||
timeZone,
|
||||
eventBus,
|
||||
orientation,
|
||||
barWidth,
|
||||
showValue,
|
||||
groupWidth,
|
||||
xTickLabelRotation,
|
||||
xTickLabelMaxLength,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text,
|
||||
rawValue,
|
||||
allFrames: props.frames,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<GraphNG
|
||||
{...props}
|
||||
theme={theme}
|
||||
frames={props.frames}
|
||||
prepConfig={prepConfig}
|
||||
propsToDiff={propsToDiff}
|
||||
preparePlotFrame={preparePlotFrame}
|
||||
renderLegend={renderLegend}
|
||||
/>
|
||||
);
|
||||
};
|
||||
BarChart.displayName = 'BarChart';
|
@ -1,26 +1,84 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { TooltipDisplayMode, StackingMode } from '@grafana/schema';
|
||||
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
||||
import { measureText, TooltipPlugin, UPLOT_AXIS_FONT_SIZE, useTheme2 } from '@grafana/ui';
|
||||
import { BarChartOptions } from './types';
|
||||
import { BarChart } from './BarChart';
|
||||
import { prepareGraphableFrames } from './utils';
|
||||
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';
|
||||
|
||||
interface Props extends PanelProps<BarChartOptions> {}
|
||||
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 frames = useMemo(() => prepareGraphableFrames(data?.series, theme, options), [data, theme, options]);
|
||||
const orientation = useMemo(() => {
|
||||
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
||||
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
||||
}
|
||||
|
||||
return options.orientation;
|
||||
}, [width, height, options.orientation]);
|
||||
|
||||
@ -49,25 +107,122 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
||||
return options.tooltip;
|
||||
}, [options.tooltip, options.stacking]);
|
||||
|
||||
if (!frames) {
|
||||
return <PanelDataErrorView panelId={id} data={data} needsStringField={true} needsNumberField={true} />;
|
||||
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 (
|
||||
<BarChart
|
||||
frames={frames}
|
||||
<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={data.structureRev}
|
||||
structureRev={structureRev}
|
||||
width={width}
|
||||
height={height}
|
||||
{...options}
|
||||
orientation={orientation}
|
||||
xTickLabelMaxLength={xTickLabelMaxLength}
|
||||
>
|
||||
{(config, alignedFrame) => {
|
||||
return <TooltipPlugin data={alignedFrame} config={config} mode={tooltip.mode} timeZone={timeZone} />;
|
||||
return (
|
||||
<TooltipPlugin
|
||||
data={alignedFrame}
|
||||
config={config}
|
||||
mode={tooltip.mode}
|
||||
timeZone={timeZone}
|
||||
renderTooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</BarChart>
|
||||
</GraphNG>
|
||||
);
|
||||
};
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
VizLegendOptions,
|
||||
} from '@grafana/schema';
|
||||
import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||
import { formatTime } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
|
||||
|
||||
const groupDistr = SPACE_BETWEEN;
|
||||
const barDistr = SPACE_BETWEEN;
|
||||
@ -40,27 +42,32 @@ export interface BarsOptions {
|
||||
xDir: ScaleDirection;
|
||||
groupWidth: number;
|
||||
barWidth: number;
|
||||
barRadius: number;
|
||||
showValue: VisibilityMode;
|
||||
stacking: StackingMode;
|
||||
rawValue: (seriesIdx: number, valueIdx: number) => number | null;
|
||||
getColor?: (seriesIdx: number, valueIdx: number, value: any) => string | null;
|
||||
fillOpacity?: number;
|
||||
formatValue: (seriesIdx: number, value: any) => string;
|
||||
text?: VizTextDisplayOptions;
|
||||
onHover?: (seriesIdx: number, valueIdx: number) => void;
|
||||
onLeave?: (seriesIdx: number, valueIdx: number) => void;
|
||||
legend?: VizLegendOptions;
|
||||
xSpacing?: number;
|
||||
xTimeAuto?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
const { xOri, xDir: dir, rawValue, formatValue, showValue } = opts;
|
||||
const { xOri, xDir: dir, rawValue, getColor, formatValue, fillOpacity = 1, showValue, xSpacing = 0 } = opts;
|
||||
const isXHorizontal = xOri === ScaleOrientation.Horizontal;
|
||||
const hasAutoValueSize = !Boolean(opts.text?.valueSize);
|
||||
const isStacked = opts.stacking !== StackingMode.None;
|
||||
const pctStacked = opts.stacking === StackingMode.Percent;
|
||||
|
||||
let { groupWidth, barWidth } = opts;
|
||||
let { groupWidth, barWidth, barRadius = 0 } = opts;
|
||||
|
||||
if (isStacked) {
|
||||
[groupWidth, barWidth] = [barWidth, groupWidth];
|
||||
@ -75,16 +82,56 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
barMark.style.background = 'rgba(255,255,255,0.4)';
|
||||
|
||||
const xSplits: Axis.Splits = (u: uPlot) => {
|
||||
const dim = isXHorizontal ? u.bbox.width : u.bbox.height;
|
||||
const _dir = dir * (isXHorizontal ? 1 : -1);
|
||||
|
||||
let dataLen = u.data[0].length;
|
||||
let lastIdx = dataLen - 1;
|
||||
|
||||
let skipMod = 0;
|
||||
|
||||
if (xSpacing !== 0) {
|
||||
let cssDim = dim / devicePixelRatio;
|
||||
let maxTicks = Math.abs(Math.floor(cssDim / xSpacing));
|
||||
|
||||
skipMod = dataLen < maxTicks ? 0 : Math.ceil(dataLen / maxTicks);
|
||||
}
|
||||
|
||||
let splits: number[] = [];
|
||||
|
||||
// for distr: 2 scales, the splits array should contain indices into data[0] rather than values
|
||||
let splits = u.data[0].map((v, i) => i);
|
||||
u.data[0].forEach((v, i) => {
|
||||
let shouldSkip = skipMod !== 0 && (xSpacing > 0 ? i : lastIdx - i) % skipMod > 0;
|
||||
|
||||
if (!shouldSkip) {
|
||||
splits.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
return _dir === 1 ? splits : splits.reverse();
|
||||
};
|
||||
|
||||
// the splits passed into here are data[0] values looked up by the indices returned from splits()
|
||||
const xValues: Axis.Values = (u, splits) => {
|
||||
const xValues: Axis.Values = (u, splits, axisIdx, foundSpace, foundIncr) => {
|
||||
if (opts.xTimeAuto) {
|
||||
// bit of a hack:
|
||||
// temporarily set x scale range to temporal (as expected by formatTime()) rather than ordinal
|
||||
let xScale = u.scales.x;
|
||||
let oMin = xScale.min;
|
||||
let oMax = xScale.max;
|
||||
|
||||
xScale.min = u.data[0][0];
|
||||
xScale.max = u.data[0][u.data[0].length - 1];
|
||||
|
||||
let vals = formatTime(u, splits, axisIdx, foundSpace, foundIncr);
|
||||
|
||||
// revert
|
||||
xScale.min = oMin;
|
||||
xScale.max = oMax;
|
||||
|
||||
return vals;
|
||||
}
|
||||
|
||||
return splits.map((v) => formatValue(0, v));
|
||||
};
|
||||
|
||||
@ -145,13 +192,30 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
};
|
||||
|
||||
let barsPctLayout: Array<null | { offs: number[]; size: number[] }> = [];
|
||||
let barsColors: Array<null | { fill: Array<string | null>; stroke: Array<string | null> }> = [];
|
||||
let barRects: Rect[] = [];
|
||||
|
||||
// minimum available space for labels between bar end and plotting area bound (in canvas pixels)
|
||||
let vSpace = Infinity;
|
||||
let hSpace = Infinity;
|
||||
|
||||
let useMappedColors = getColor != null;
|
||||
|
||||
let mappedColorDisp = useMappedColors
|
||||
? {
|
||||
fill: {
|
||||
unit: 3,
|
||||
values: (u: uPlot, seriesIdx: number) => barsColors[seriesIdx]!.fill,
|
||||
},
|
||||
stroke: {
|
||||
unit: 3,
|
||||
values: (u: uPlot, seriesIdx: number) => barsColors[seriesIdx]!.stroke,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
let barsBuilder = uPlot.paths.bars!({
|
||||
radius: barRadius,
|
||||
disp: {
|
||||
x0: {
|
||||
unit: 2,
|
||||
@ -161,6 +225,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
unit: 2,
|
||||
values: (u, seriesIdx) => barsPctLayout[seriesIdx]!.size,
|
||||
},
|
||||
...mappedColorDisp,
|
||||
},
|
||||
// collect rendered bar geometry
|
||||
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||
@ -210,6 +275,27 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
} else {
|
||||
barsPctLayout = [null as any].concat(distrTwo(u.data[0].length, u.data.length - 1));
|
||||
}
|
||||
|
||||
if (useMappedColors) {
|
||||
barsColors = [null];
|
||||
|
||||
// map per-bar colors
|
||||
for (let i = 1; i < u.data.length; i++) {
|
||||
let colors = u.data[i].map((value, valueIdx) => {
|
||||
if (value != null) {
|
||||
return getColor!(i, valueIdx, value);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
barsColors.push({
|
||||
fill: fillOpacity < 1 ? colors.map((c) => (c != null ? alpha(c, fillOpacity) : null)) : colors,
|
||||
stroke: colors,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
barRects.length = 0;
|
||||
vSpace = hSpace = Infinity;
|
||||
};
|
||||
|
49
public/app/plugins/panel/barchart/models.gen.ts
Normal file
49
public/app/plugins/panel/barchart/models.gen.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import {
|
||||
OptionsWithLegend,
|
||||
OptionsWithTextFormatting,
|
||||
OptionsWithTooltip,
|
||||
AxisConfig,
|
||||
VisibilityMode,
|
||||
GraphGradientMode,
|
||||
HideableFieldConfig,
|
||||
StackingMode,
|
||||
} from '@grafana/schema';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
|
||||
export interface PanelOptions extends OptionsWithLegend, OptionsWithTooltip, OptionsWithTextFormatting {
|
||||
xField?: string;
|
||||
colorByField?: string;
|
||||
orientation: VizOrientation;
|
||||
stacking: StackingMode;
|
||||
showValue: VisibilityMode;
|
||||
barWidth: number;
|
||||
barRadius?: number;
|
||||
groupWidth: number;
|
||||
xTickLabelRotation: number;
|
||||
xTickLabelMaxLength: number;
|
||||
xTickLabelSpacing?: number; // negative values indicate backwards skipping behavior
|
||||
}
|
||||
|
||||
export const defaultPanelOptions: Partial<PanelOptions> = {
|
||||
stacking: StackingMode.None,
|
||||
orientation: VizOrientation.Auto,
|
||||
xTickLabelRotation: 0,
|
||||
xTickLabelMaxLength: 0,
|
||||
showValue: VisibilityMode.Auto,
|
||||
groupWidth: 0.7,
|
||||
barWidth: 0.97,
|
||||
barRadius: 0,
|
||||
};
|
||||
|
||||
export interface BarChartFieldConfig extends AxisConfig, HideableFieldConfig {
|
||||
lineWidth?: number; // 0
|
||||
fillOpacity?: number; // 100
|
||||
gradientMode?: GraphGradientMode;
|
||||
}
|
||||
|
||||
export const defaultBarChartFieldConfig: BarChartFieldConfig = {
|
||||
lineWidth: 1,
|
||||
fillOpacity: 80,
|
||||
gradientMode: GraphGradientMode.None,
|
||||
axisSoftMin: 0,
|
||||
};
|
@ -3,16 +3,19 @@ import {
|
||||
FieldColorModeId,
|
||||
FieldConfigProperty,
|
||||
FieldType,
|
||||
getFieldDisplayName,
|
||||
PanelPlugin,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { BarChartPanel } from './BarChartPanel';
|
||||
import { StackingMode, VisibilityMode } from '@grafana/schema';
|
||||
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
|
||||
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from 'app/plugins/panel/barchart/types';
|
||||
import { BarChartFieldConfig, PanelOptions, defaultBarChartFieldConfig, defaultPanelOptions } from './models.gen';
|
||||
import { BarChartSuggestionsSupplier } from './suggestions';
|
||||
import { prepareBarChartDisplayValues } from './utils';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarChartPanel)
|
||||
export const plugin = new PanelPlugin<PanelOptions, BarChartFieldConfig>(BarChartPanel)
|
||||
.useFieldConfig({
|
||||
standardOptions: {
|
||||
[FieldConfigProperty.Color]: {
|
||||
@ -62,8 +65,22 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
commonOptionsBuilder.addHideFrom(builder);
|
||||
},
|
||||
})
|
||||
.setPanelOptions((builder) => {
|
||||
.setPanelOptions((builder, context) => {
|
||||
const disp = prepareBarChartDisplayValues(context.data, config.theme2, context.options ?? ({} as any));
|
||||
let xaxisPlaceholder = 'First string or time field';
|
||||
if (disp.viz?.fields?.length) {
|
||||
const first = disp.viz.fields[0];
|
||||
xaxisPlaceholder += ` (${getFieldDisplayName(first, disp.viz)})`;
|
||||
}
|
||||
|
||||
builder
|
||||
.addFieldNamePicker({
|
||||
path: 'xField',
|
||||
name: 'X Axis',
|
||||
settings: {
|
||||
placeholderText: xaxisPlaceholder,
|
||||
},
|
||||
})
|
||||
.addRadio({
|
||||
path: 'orientation',
|
||||
name: 'Orientation',
|
||||
@ -74,12 +91,12 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
{ value: VizOrientation.Vertical, label: 'Vertical' },
|
||||
],
|
||||
},
|
||||
defaultValue: VizOrientation.Auto,
|
||||
defaultValue: defaultPanelOptions.orientation,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'xTickLabelRotation',
|
||||
name: 'Rotate bar labels',
|
||||
defaultValue: 0,
|
||||
defaultValue: defaultPanelOptions.xTickLabelRotation,
|
||||
settings: {
|
||||
min: -90,
|
||||
max: 90,
|
||||
@ -100,6 +117,19 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
min: 0,
|
||||
},
|
||||
})
|
||||
// .addSliderInput({
|
||||
// path: 'xTickLabelSpacing',
|
||||
// name: 'Bar label minimum spacing',
|
||||
// description: 'Bar labels will be skipped to maintain this distance',
|
||||
// defaultValue: 0,
|
||||
// settings: {
|
||||
// min: -300,
|
||||
// max: 300,
|
||||
// step: 10,
|
||||
// marks: { '-300': 'Backward', 0: 'None', 300: 'Forward' },
|
||||
// included: false,
|
||||
// },
|
||||
// })
|
||||
.addRadio({
|
||||
path: 'showValue',
|
||||
name: 'Show values',
|
||||
@ -110,7 +140,7 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
{ value: VisibilityMode.Never, label: 'Never' },
|
||||
],
|
||||
},
|
||||
defaultValue: VisibilityMode.Auto,
|
||||
defaultValue: defaultPanelOptions.showValue,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'stacking',
|
||||
@ -118,12 +148,12 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
settings: {
|
||||
options: graphFieldOptions.stacking,
|
||||
},
|
||||
defaultValue: StackingMode.None,
|
||||
defaultValue: defaultPanelOptions.stacking,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'groupWidth',
|
||||
name: 'Group width',
|
||||
defaultValue: 0.7,
|
||||
defaultValue: defaultPanelOptions.groupWidth,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
@ -139,14 +169,30 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
.addSliderInput({
|
||||
path: 'barWidth',
|
||||
name: 'Bar width',
|
||||
defaultValue: 0.97,
|
||||
defaultValue: defaultPanelOptions.barWidth,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'barRadius',
|
||||
name: 'Bar radius',
|
||||
defaultValue: defaultPanelOptions.barRadius,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 0.5,
|
||||
step: 0.05,
|
||||
},
|
||||
});
|
||||
|
||||
builder.addFieldNamePicker({
|
||||
path: 'colorByField',
|
||||
name: 'Color by field',
|
||||
description: 'Use the color value for a sibling field to color each bar value.',
|
||||
});
|
||||
|
||||
commonOptionsBuilder.addTooltipOptions(builder);
|
||||
commonOptionsBuilder.addLegendOptions(builder);
|
||||
commonOptionsBuilder.addTextSizeOptions(builder, false);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { VisualizationSuggestionsBuilder, VizOrientation } from '@grafana/data';
|
||||
import { LegendDisplayMode, StackingMode, VisibilityMode } from '@grafana/schema';
|
||||
import { SuggestionName } from 'app/types/suggestions';
|
||||
import { BarChartFieldConfig, BarChartOptions } from './types';
|
||||
import { BarChartFieldConfig, PanelOptions } from './models.gen';
|
||||
|
||||
export class BarChartSuggestionsSupplier {
|
||||
getListWithDefaults(builder: VisualizationSuggestionsBuilder) {
|
||||
return builder.getListAppender<BarChartOptions, BarChartFieldConfig>({
|
||||
return builder.getListAppender<PanelOptions, BarChartFieldConfig>({
|
||||
name: SuggestionName.BarChart,
|
||||
pluginId: 'barchart',
|
||||
options: {
|
||||
|
@ -1,44 +1,15 @@
|
||||
import {
|
||||
OptionsWithLegend,
|
||||
OptionsWithTextFormatting,
|
||||
OptionsWithTooltip,
|
||||
AxisConfig,
|
||||
VisibilityMode,
|
||||
GraphGradientMode,
|
||||
HideableFieldConfig,
|
||||
StackingMode,
|
||||
} from '@grafana/schema';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
import { DataFrame, Field } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface BarChartOptions extends OptionsWithLegend, OptionsWithTooltip, OptionsWithTextFormatting {
|
||||
orientation: VizOrientation;
|
||||
stacking: StackingMode;
|
||||
showValue: VisibilityMode;
|
||||
barWidth: number;
|
||||
groupWidth: number;
|
||||
xTickLabelRotation: number;
|
||||
xTickLabelMaxLength: number;
|
||||
rawValue: (seriesIdx: number, valueIdx: number) => number;
|
||||
export interface BarChartDisplayValues {
|
||||
/** When the data can not display, this will be returned */
|
||||
warn?: string;
|
||||
|
||||
/** All fields joined */
|
||||
aligned: DataFrame;
|
||||
|
||||
/** The fields we can display, first field is X axis */
|
||||
viz: DataFrame;
|
||||
|
||||
/** Potentialy color by a field value */
|
||||
colorByField?: Field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface BarChartFieldConfig extends AxisConfig, HideableFieldConfig {
|
||||
lineWidth?: number; // 0
|
||||
fillOpacity?: number; // 100
|
||||
gradientMode?: GraphGradientMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const defaultBarChartFieldConfig: BarChartFieldConfig = {
|
||||
lineWidth: 1,
|
||||
fillOpacity: 80,
|
||||
gradientMode: GraphGradientMode.None,
|
||||
axisSoftMin: 0,
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { prepareGraphableFrames, preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||
import { BarChartOptionsEX, prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
|
||||
import {
|
||||
LegendDisplayMode,
|
||||
TooltipDisplayMode,
|
||||
@ -16,7 +16,7 @@ import {
|
||||
MutableDataFrame,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { BarChartFieldConfig, BarChartOptions } from './types';
|
||||
import { BarChartFieldConfig } from './models.gen';
|
||||
|
||||
function mockDataFrame() {
|
||||
const df1 = new MutableDataFrame({
|
||||
@ -65,7 +65,7 @@ function mockDataFrame() {
|
||||
state: {},
|
||||
});
|
||||
|
||||
return preparePlotFrame([df1, df2]);
|
||||
return prepareBarChartDisplayValues([df1], createTheme(), {} as any).aligned;
|
||||
}
|
||||
|
||||
jest.mock('@grafana/data', () => ({
|
||||
@ -77,7 +77,7 @@ describe('BarChart utils', () => {
|
||||
describe('preparePlotConfigBuilder', () => {
|
||||
const frame = mockDataFrame();
|
||||
|
||||
const config: BarChartOptions = {
|
||||
const config: BarChartOptionsEX = {
|
||||
orientation: VizOrientation.Auto,
|
||||
groupWidth: 20,
|
||||
barWidth: 2,
|
||||
@ -146,19 +146,20 @@ describe('BarChart utils', () => {
|
||||
|
||||
describe('prepareGraphableFrames', () => {
|
||||
it('will warn when there is no data in the response', () => {
|
||||
const result = prepareGraphableFrames([], createTheme(), { stacking: StackingMode.None } as any);
|
||||
expect(result).toBeNull();
|
||||
const result = prepareBarChartDisplayValues([], createTheme(), { stacking: StackingMode.None } as any);
|
||||
expect(result.warn).toEqual('No data in response');
|
||||
});
|
||||
|
||||
it('will warn when there is no string field in the response', () => {
|
||||
it('will warn when there is no string or time field', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'a', type: FieldType.time, values: [1, 2, 3, 4, 5] },
|
||||
{ name: 'a', type: FieldType.other, values: [1, 2, 3, 4, 5] },
|
||||
{ name: 'value', values: [1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
const result = prepareGraphableFrames([df], createTheme(), { stacking: StackingMode.None } as any);
|
||||
expect(result).toBeNull();
|
||||
const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as any);
|
||||
expect(result.warn).toEqual('Bar charts requires a string or time field');
|
||||
expect(result.viz).toBeUndefined();
|
||||
});
|
||||
|
||||
it('will warn when there are no numeric fields in the response', () => {
|
||||
@ -168,8 +169,9 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', type: FieldType.boolean, values: [true, true, true, true, true] },
|
||||
],
|
||||
});
|
||||
const result = prepareGraphableFrames([df], createTheme(), { stacking: StackingMode.None } as any);
|
||||
expect(result).toBeNull();
|
||||
const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as any);
|
||||
expect(result.warn).toEqual('No numeric fields found');
|
||||
expect(result.viz).toBeUndefined();
|
||||
});
|
||||
|
||||
it('will convert NaN and Infinty to nulls', () => {
|
||||
@ -179,9 +181,9 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', values: [-10, NaN, 10, -Infinity, +Infinity] },
|
||||
],
|
||||
});
|
||||
const frames = prepareGraphableFrames([df], createTheme(), { stacking: StackingMode.None } as any)!;
|
||||
const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as any);
|
||||
|
||||
const field = frames[0].fields[1];
|
||||
const field = result.viz.fields[1];
|
||||
expect(field!.values.toArray()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
-10,
|
||||
@ -203,23 +205,21 @@ describe('BarChart utils', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const framesAsc = prepareGraphableFrames([frame], createTheme(), {
|
||||
const resultAsc = prepareBarChartDisplayValues([frame], createTheme(), {
|
||||
legend: { sortBy: 'Min', sortDesc: false },
|
||||
} as any)!;
|
||||
} as any);
|
||||
expect(resultAsc.viz.fields[0].type).toBe(FieldType.string);
|
||||
expect(resultAsc.viz.fields[1].name).toBe('a');
|
||||
expect(resultAsc.viz.fields[2].name).toBe('c');
|
||||
expect(resultAsc.viz.fields[3].name).toBe('b');
|
||||
|
||||
expect(framesAsc[0].fields[0].type).toBe(FieldType.string);
|
||||
expect(framesAsc[0].fields[1].name).toBe('a');
|
||||
expect(framesAsc[0].fields[2].name).toBe('c');
|
||||
expect(framesAsc[0].fields[3].name).toBe('b');
|
||||
|
||||
const framesDesc = prepareGraphableFrames([frame], createTheme(), {
|
||||
const resultDesc = prepareBarChartDisplayValues([frame], createTheme(), {
|
||||
legend: { sortBy: 'Min', sortDesc: true },
|
||||
} as any)!;
|
||||
|
||||
expect(framesDesc[0].fields[0].type).toBe(FieldType.string);
|
||||
expect(framesDesc[0].fields[1].name).toBe('b');
|
||||
expect(framesDesc[0].fields[2].name).toBe('c');
|
||||
expect(framesDesc[0].fields[3].name).toBe('a');
|
||||
} as any);
|
||||
expect(resultDesc.viz.fields[0].type).toBe(FieldType.string);
|
||||
expect(resultDesc.viz.fields[1].name).toBe('b');
|
||||
expect(resultDesc.viz.fields[2].name).toBe('c');
|
||||
expect(resultDesc.viz.fields[3].name).toBe('a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,10 +8,12 @@ import {
|
||||
getFieldColorModeForField,
|
||||
getFieldSeriesColor,
|
||||
GrafanaTheme2,
|
||||
MutableDataFrame,
|
||||
outerJoinDataFrames,
|
||||
reduceField,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from './types';
|
||||
import { BarChartFieldConfig, PanelOptions, defaultBarChartFieldConfig } from './models.gen';
|
||||
import { BarChartDisplayValues } from './types';
|
||||
import { BarsOptions, getConfig } from './bars';
|
||||
import { FIXED_UNIT, measureText, UPlotConfigBuilder, UPlotConfigPrepFn, UPLOT_AXIS_FONT_SIZE } from '@grafana/ui';
|
||||
import { Padding } from 'uplot';
|
||||
@ -25,8 +27,8 @@ import {
|
||||
} from '@grafana/schema';
|
||||
import { collectStackingGroups, orderIdsByCalcs } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { orderBy } from 'lodash';
|
||||
import { findField } from 'app/features/dimensions';
|
||||
|
||||
/** @alpha */
|
||||
function getBarCharScaleOrientation(orientation: VizOrientation) {
|
||||
if (orientation === VizOrientation.Vertical) {
|
||||
return {
|
||||
@ -45,19 +47,29 @@ function getBarCharScaleOrientation(orientation: VizOrientation) {
|
||||
};
|
||||
}
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
export interface BarChartOptionsEX extends PanelOptions {
|
||||
rawValue: (seriesIdx: number, valueIdx: number) => number | null;
|
||||
getColor?: (seriesIdx: number, valueIdx: number, value: any) => string | null;
|
||||
fillOpacity?: number;
|
||||
}
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
frame,
|
||||
theme,
|
||||
orientation,
|
||||
showValue,
|
||||
groupWidth,
|
||||
barWidth,
|
||||
barRadius = 0,
|
||||
stacking,
|
||||
text,
|
||||
rawValue,
|
||||
getColor,
|
||||
fillOpacity,
|
||||
allFrames,
|
||||
xTickLabelRotation,
|
||||
xTickLabelMaxLength,
|
||||
xTickLabelSpacing = 0,
|
||||
legend,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
@ -81,12 +93,17 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
xDir: vizOrientation.xDir,
|
||||
groupWidth,
|
||||
barWidth,
|
||||
barRadius,
|
||||
stacking,
|
||||
rawValue,
|
||||
getColor,
|
||||
fillOpacity,
|
||||
formatValue,
|
||||
text,
|
||||
showValue,
|
||||
legend,
|
||||
xSpacing: xTickLabelSpacing,
|
||||
xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'),
|
||||
};
|
||||
|
||||
const config = getConfig(opts, theme);
|
||||
@ -275,60 +292,66 @@ function getRotationPadding(frame: DataFrame, rotateLabel: number, valueMaxLengt
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotFrame(data: DataFrame[]) {
|
||||
const firstFrame = data[0];
|
||||
const firstString = firstFrame.fields.find((f) => f.type === FieldType.string);
|
||||
|
||||
if (!firstString) {
|
||||
throw new Error('No string field in DF');
|
||||
export function prepareBarChartDisplayValues(
|
||||
series: DataFrame[],
|
||||
theme: GrafanaTheme2,
|
||||
options: PanelOptions
|
||||
): BarChartDisplayValues {
|
||||
if (!series?.length) {
|
||||
return { warn: 'No data in response' } as BarChartDisplayValues;
|
||||
}
|
||||
|
||||
const resultFrame = new MutableDataFrame();
|
||||
resultFrame.addField(firstString);
|
||||
// Bar chart requires a single frame
|
||||
const frame = series.length === 1 ? series[0] : outerJoinDataFrames({ frames: series, enforceSort: false });
|
||||
if (!frame) {
|
||||
return { warn: 'Unable to join data' } as BarChartDisplayValues;
|
||||
}
|
||||
|
||||
for (const f of firstFrame.fields) {
|
||||
if (f.type === FieldType.number) {
|
||||
resultFrame.addField(f);
|
||||
// Color by a field different than the input
|
||||
let colorByField: Field | undefined = undefined;
|
||||
if (options.colorByField) {
|
||||
colorByField = findField(frame, options.colorByField);
|
||||
if (!colorByField) {
|
||||
return { warn: 'Color field not found' } as BarChartDisplayValues;
|
||||
}
|
||||
}
|
||||
|
||||
return resultFrame;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function prepareGraphableFrames(
|
||||
series: DataFrame[],
|
||||
theme: GrafanaTheme2,
|
||||
options: BarChartOptions
|
||||
): DataFrame[] | null {
|
||||
if (!series?.length) {
|
||||
return null;
|
||||
let xField: Field | undefined = undefined;
|
||||
if (options.xField) {
|
||||
xField = findField(frame, options.xField);
|
||||
if (!xField) {
|
||||
return { warn: 'Configured x field not found' } as BarChartDisplayValues;
|
||||
}
|
||||
}
|
||||
|
||||
const frames: DataFrame[] = [];
|
||||
const firstFrame = series[0];
|
||||
let stringField: Field | undefined = undefined;
|
||||
let timeField: Field | undefined = undefined;
|
||||
let fields: Field[] = [];
|
||||
for (const field of frame.fields) {
|
||||
if (field === xField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!firstFrame.fields.some((f) => f.type === FieldType.string)) {
|
||||
return null;
|
||||
}
|
||||
switch (field.type) {
|
||||
case FieldType.string:
|
||||
if (!stringField) {
|
||||
stringField = field;
|
||||
}
|
||||
break;
|
||||
|
||||
if (!firstFrame.fields.some((f) => f.type === FieldType.number)) {
|
||||
return null;
|
||||
}
|
||||
case FieldType.time:
|
||||
if (!timeField) {
|
||||
timeField = field;
|
||||
}
|
||||
break;
|
||||
|
||||
const legendOrdered = isLegendOrdered(options.legend);
|
||||
let seriesIndex = 0;
|
||||
|
||||
for (let frame of series) {
|
||||
const fields: Field[] = [];
|
||||
for (const field of frame.fields) {
|
||||
if (field.type === FieldType.number) {
|
||||
field.state = field.state ?? {};
|
||||
|
||||
field.state.seriesIndex = seriesIndex++;
|
||||
|
||||
let copy = {
|
||||
case FieldType.number: {
|
||||
const copy = {
|
||||
...field,
|
||||
state: {
|
||||
...field.state,
|
||||
seriesIndex: fields.length, // off by one?
|
||||
},
|
||||
config: {
|
||||
...field.config,
|
||||
custom: {
|
||||
@ -355,34 +378,58 @@ export function prepareGraphableFrames(
|
||||
}
|
||||
|
||||
fields.push(copy);
|
||||
} else {
|
||||
fields.push({ ...field });
|
||||
}
|
||||
}
|
||||
|
||||
let orderedFields: Field[] | undefined;
|
||||
|
||||
if (legendOrdered) {
|
||||
orderedFields = orderBy(
|
||||
fields,
|
||||
({ state }) => {
|
||||
return state?.calcs?.[options.legend.sortBy!.toLowerCase()];
|
||||
},
|
||||
options.legend.sortDesc ? 'desc' : 'asc'
|
||||
);
|
||||
// The string field needs to be the first one
|
||||
if (orderedFields[orderedFields.length - 1].type === FieldType.string) {
|
||||
orderedFields.unshift(orderedFields.pop()!);
|
||||
}
|
||||
}
|
||||
|
||||
frames.push({
|
||||
...frame,
|
||||
fields: orderedFields || fields,
|
||||
});
|
||||
}
|
||||
|
||||
return frames;
|
||||
let firstField = xField;
|
||||
if (!firstField) {
|
||||
firstField = stringField || timeField;
|
||||
}
|
||||
|
||||
if (!firstField) {
|
||||
return {
|
||||
warn: 'Bar charts requires a string or time field',
|
||||
} as BarChartDisplayValues;
|
||||
}
|
||||
|
||||
if (!fields.length) {
|
||||
return {
|
||||
warn: 'No numeric fields found',
|
||||
} as BarChartDisplayValues;
|
||||
}
|
||||
|
||||
// Show the first number value
|
||||
if (colorByField && fields.length > 1) {
|
||||
const firstNumber = fields.find((f) => f !== colorByField);
|
||||
if (firstNumber) {
|
||||
fields = [firstNumber];
|
||||
}
|
||||
}
|
||||
|
||||
if (isLegendOrdered(options.legend)) {
|
||||
const sortKey = options.legend.sortBy!.toLowerCase();
|
||||
const reducers = options.legend.calcs ?? [sortKey];
|
||||
fields = orderBy(
|
||||
fields,
|
||||
(field) => {
|
||||
return reduceField({ field, reducers })[sortKey];
|
||||
},
|
||||
options.legend.sortDesc ? 'desc' : 'asc'
|
||||
);
|
||||
}
|
||||
|
||||
// String field is first
|
||||
fields.unshift(firstField);
|
||||
|
||||
return {
|
||||
aligned: frame,
|
||||
colorByField,
|
||||
viz: {
|
||||
length: firstField.values.length,
|
||||
fields: fields, // ideally: fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.viz)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const isLegendOrdered = (options: VizLegendOptions) => Boolean(options?.sortBy && options.sortDesc !== null);
|
||||
|
@ -15,8 +15,8 @@ import { FeatureLike } from 'ol/Feature';
|
||||
export interface Props {
|
||||
data?: DataFrame; // source data
|
||||
feature?: FeatureLike;
|
||||
rowIndex?: number; // the hover row
|
||||
columnIndex?: number; // the hover column
|
||||
rowIndex?: number | null; // the hover row
|
||||
columnIndex?: number | null; // the hover column
|
||||
}
|
||||
|
||||
export class DataHoverView extends PureComponent<Props> {
|
||||
|
@ -485,7 +485,10 @@ export function prepareTimelineLegendItems(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fields = allNonTimeFields(frames);
|
||||
return getFieldLegendItem(allNonTimeFields(frames), theme);
|
||||
}
|
||||
|
||||
export function getFieldLegendItem(fields: Field[], theme: GrafanaTheme2): VizLegendItem[] | undefined {
|
||||
if (!fields.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user