mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: Refactor and VizTooltip fixes (#87160)
Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
parent
8bfd7e5106
commit
f43ed7e6d7
@ -4484,22 +4484,18 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/annolist/AnnoListPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/BarChartPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/TickSpacingEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/bars.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/module.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/quadtree.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/utils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/candlestick/CandlestickPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
|
@ -88,7 +88,7 @@
|
||||
"id": "links",
|
||||
"value": [
|
||||
{
|
||||
"title": "google",
|
||||
"title": "${__data.fields.id}/${__field.name}/${__value.raw}",
|
||||
"url": "google.com"
|
||||
},
|
||||
{
|
||||
@ -114,8 +114,8 @@
|
||||
"id": "links",
|
||||
"value": [
|
||||
{
|
||||
"title": "datalink column 2",
|
||||
"url": "grafana.com"
|
||||
"title": "${__data.fields.id}/${__field.name}/${__value.raw}",
|
||||
"url": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -906,6 +906,6 @@
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Bar Chart Tooltips & Legends",
|
||||
"uid": "ea33320b-bd97-4fe1-a27c-24bc61a48b41",
|
||||
"version": 1,
|
||||
"version": 5,
|
||||
"weekStart": ""
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { FALLBACK_COLOR, Field, FieldType, formattedValueToString } from '@grafana/data';
|
||||
import { FALLBACK_COLOR, Field, FieldType, formattedValueToString, getFieldColorModeForField } from '@grafana/data';
|
||||
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { ColorIndicatorStyles } from './VizTooltipColorIndicator';
|
||||
@ -131,12 +131,22 @@ export const getContentItems = (
|
||||
? Number.MIN_SAFE_INTEGER
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const colorMode = getFieldColorModeForField(field);
|
||||
|
||||
let colorIndicator = ColorIndicator.series;
|
||||
let colorPlacement = ColorPlacement.first;
|
||||
|
||||
if (colorMode.isByValue) {
|
||||
colorIndicator = ColorIndicator.value;
|
||||
colorPlacement = ColorPlacement.trailing;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
label: field.state?.displayName ?? field.name,
|
||||
value: formattedValueToString(display),
|
||||
color: display.color ?? FALLBACK_COLOR,
|
||||
colorIndicator: ColorIndicator.series,
|
||||
colorPlacement: ColorPlacement.first,
|
||||
colorIndicator,
|
||||
colorPlacement,
|
||||
isActive: mode === TooltipDisplayMode.Multi && seriesIdx === i,
|
||||
numeric,
|
||||
});
|
||||
|
93
public/app/plugins/panel/barchart/BarChartLegend.tsx
Normal file
93
public/app/plugins/panel/barchart/BarChartLegend.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, Field, getFieldSeriesColor } from '@grafana/data';
|
||||
import { VizLegendOptions, AxisPlacement } from '@grafana/schema';
|
||||
import { UPlotConfigBuilder, VizLayout, VizLayoutLegendProps, VizLegend, VizLegendItem, useTheme2 } from '@grafana/ui';
|
||||
import { getDisplayValuesForCalcs } from '@grafana/ui/src/components/uPlot/utils';
|
||||
import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils';
|
||||
|
||||
interface BarChartLegend2Props extends VizLegendOptions, Omit<VizLayoutLegendProps, 'children'> {
|
||||
data: DataFrame[];
|
||||
colorField?: Field | null;
|
||||
// config: UPlotConfigBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* mostly duplicates logic in PlotLegend below :(
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function hasVisibleLegendSeries(config: UPlotConfigBuilder, data: DataFrame[]) {
|
||||
return data[0].fields.slice(1).some((field) => !Boolean(field.config.custom?.hideFrom?.legend));
|
||||
|
||||
// return config.getSeries().some((s, i) => {
|
||||
// const frameIndex = 0;
|
||||
// const fieldIndex = i + 1;
|
||||
// const field = data[frameIndex].fields[fieldIndex];
|
||||
// return !Boolean(field.config.custom?.hideFrom?.legend);
|
||||
// });
|
||||
}
|
||||
|
||||
export const BarChartLegend = React.memo(
|
||||
({ data, placement, calcs, displayMode, colorField, ...vizLayoutLegendProps }: BarChartLegend2Props) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
if (colorField != null) {
|
||||
const items = getFieldLegendItem([colorField], theme);
|
||||
|
||||
if (items?.length) {
|
||||
return (
|
||||
<VizLayout.Legend placement={placement}>
|
||||
<VizLegend placement={placement} items={items} displayMode={displayMode} />
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const legendItems = data[0].fields
|
||||
.slice(1)
|
||||
.map((field, i) => {
|
||||
const frameIndex = 0;
|
||||
const fieldIndex = i + 1;
|
||||
// const axisPlacement = config.getAxisPlacement(s.props.scaleKey); // TODO: this should be stamped on the field.config?
|
||||
// const field = data[frameIndex].fields[fieldIndex];
|
||||
|
||||
if (!field || field.config.custom?.hideFrom?.legend) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// // apparently doing a second pass like this will take existing state.displayName, and if same as another one, appends counter
|
||||
// const label = getFieldDisplayName(field, data[0], data);
|
||||
const label = field.state?.displayName ?? field.name;
|
||||
|
||||
const color = getFieldSeriesColor(field, theme).color;
|
||||
|
||||
const item: VizLegendItem = {
|
||||
disabled: field.state?.hideFrom?.viz,
|
||||
color,
|
||||
label,
|
||||
yAxis: field.config.custom?.axisPlacement === AxisPlacement.Right ? 2 : 1,
|
||||
getDisplayValues: () => getDisplayValuesForCalcs(calcs, field, theme),
|
||||
getItemKey: () => `${label}-${frameIndex}-${fieldIndex}`,
|
||||
};
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter((i): i is VizLegendItem => i !== undefined);
|
||||
|
||||
return (
|
||||
<VizLayout.Legend placement={placement} {...vizLayoutLegendProps}>
|
||||
<VizLegend
|
||||
placement={placement}
|
||||
items={legendItems}
|
||||
displayMode={displayMode}
|
||||
sortBy={vizLayoutLegendProps.sortBy}
|
||||
sortDesc={vizLayoutLegendProps.sortDesc}
|
||||
isSortable={true}
|
||||
/>
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BarChartLegend.displayName = 'BarChartLegend';
|
@ -1,117 +1,134 @@
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
compareDataFrameStructures,
|
||||
DataFrame,
|
||||
Field,
|
||||
FieldColorModeId,
|
||||
FieldType,
|
||||
PanelProps,
|
||||
TimeRange,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { PanelProps, VizOrientation } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
import {
|
||||
GraphGradientMode,
|
||||
measureText,
|
||||
PlotLegend,
|
||||
TooltipDisplayMode,
|
||||
UPlotConfigBuilder,
|
||||
UPLOT_AXIS_FONT_SIZE,
|
||||
usePanelContext,
|
||||
useTheme2,
|
||||
VizLayout,
|
||||
VizLegend,
|
||||
TooltipPlugin2,
|
||||
UPLOT_AXIS_FONT_SIZE,
|
||||
UPlotChart,
|
||||
VizLayout,
|
||||
measureText,
|
||||
// usePanelContext,
|
||||
useTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
|
||||
import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG';
|
||||
import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils';
|
||||
|
||||
import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip';
|
||||
import { isTooltipScrollable } from '../timeseries/utils';
|
||||
|
||||
import { BarChartLegend, hasVisibleLegendSeries } from './BarChartLegend';
|
||||
import { Options } from './panelcfg.gen';
|
||||
import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
|
||||
import { prepConfig, prepSeries } from './utils';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface BarChartProps
|
||||
extends Options,
|
||||
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend' | 'theme'> {}
|
||||
const charWidth = measureText('M', UPLOT_AXIS_FONT_SIZE).width;
|
||||
const toRads = Math.PI / 180;
|
||||
|
||||
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,
|
||||
];
|
||||
export const BarChartPanel = (props: PanelProps<Options>) => {
|
||||
const {
|
||||
data,
|
||||
options,
|
||||
fieldConfig,
|
||||
width,
|
||||
height,
|
||||
timeZone,
|
||||
id,
|
||||
// replaceVariables
|
||||
} = props;
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
// will need this if joining on time to re-create data links
|
||||
// const { dataLinkPostProcessor } = usePanelContext();
|
||||
|
||||
export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZone, id, replaceVariables }: Props) => {
|
||||
const theme = useTheme2();
|
||||
const { dataLinkPostProcessor } = usePanelContext();
|
||||
|
||||
const frame0Ref = useRef<DataFrame>();
|
||||
const colorByFieldRef = useRef<Field>();
|
||||
const {
|
||||
barWidth,
|
||||
barRadius = 0,
|
||||
showValue,
|
||||
groupWidth,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text,
|
||||
xTickLabelRotation,
|
||||
xTickLabelSpacing,
|
||||
fullHighlight,
|
||||
xField,
|
||||
colorByField,
|
||||
} = options;
|
||||
|
||||
const info = useMemo(() => prepareBarChartDisplayValues(data.series, theme, options), [data.series, theme, options]);
|
||||
const chartDisplay = 'viz' in info ? info : null;
|
||||
// size-dependent, calculated opts that should cause viz re-config
|
||||
let { orientation, xTickLabelMaxLength = 0 } = options;
|
||||
|
||||
colorByFieldRef.current = chartDisplay?.colorByField;
|
||||
orientation =
|
||||
orientation === VizOrientation.Auto
|
||||
? width < height
|
||||
? VizOrientation.Horizontal
|
||||
: VizOrientation.Vertical
|
||||
: orientation;
|
||||
|
||||
const structureRef = useRef(10000);
|
||||
// TODO: this can be moved into axis calc internally, no need to re-config based on this
|
||||
// should be based on vizHeight, not full height?
|
||||
xTickLabelMaxLength =
|
||||
xTickLabelRotation === 0
|
||||
? Infinity // should this calc using spacing between groups?
|
||||
: xTickLabelMaxLength ||
|
||||
// auto max length clamps to half viz height, subracts 3 chars for ... ellipsis
|
||||
Math.floor(height / 2 / Math.sin(Math.abs(xTickLabelRotation * toRads)) / charWidth - 3);
|
||||
|
||||
useMemo(() => {
|
||||
structureRef.current++;
|
||||
// TODO: config data links
|
||||
const info = useMemo(
|
||||
() => prepSeries(data.series, fieldConfig, stacking, theme, xField, colorByField),
|
||||
[data.series, fieldConfig, stacking, theme, xField, colorByField]
|
||||
);
|
||||
|
||||
const vizSeries = useMemo(
|
||||
() => [
|
||||
{
|
||||
...info.series![0],
|
||||
fields: info.series![0].fields.filter((field, i) => i === 0 || !field.state?.hideFrom?.viz),
|
||||
},
|
||||
],
|
||||
[info.series]
|
||||
);
|
||||
|
||||
const xGroupsCount = vizSeries[0].length;
|
||||
const seriesCount = vizSeries[0].fields?.length;
|
||||
|
||||
let { builder, prepData } = useMemo(
|
||||
() => {
|
||||
return prepConfig({ series: vizSeries, color: info.color, orientation, options, timeZone, theme });
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options]); // change every time the options object changes (while editing)
|
||||
[
|
||||
orientation,
|
||||
timeZone,
|
||||
props.data.structureRev,
|
||||
|
||||
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]);
|
||||
seriesCount,
|
||||
xGroupsCount,
|
||||
|
||||
const orientation = useMemo(() => {
|
||||
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
||||
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
||||
}
|
||||
return options.orientation;
|
||||
}, [width, height, options.orientation]);
|
||||
barWidth,
|
||||
barRadius,
|
||||
showValue,
|
||||
groupWidth,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text?.valueSize, // cause text obj is re-created each time?
|
||||
xTickLabelRotation,
|
||||
xTickLabelSpacing,
|
||||
fullHighlight,
|
||||
xField,
|
||||
colorByField,
|
||||
xTickLabelMaxLength, // maybe not?
|
||||
// props.fieldConfig, // usePrevious hideFrom on all fields?
|
||||
]
|
||||
);
|
||||
|
||||
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;
|
||||
const plotData = useMemo(() => prepData(vizSeries, info.color), [prepData, vizSeries, info.color]);
|
||||
|
||||
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) {
|
||||
if (info.warn != null) {
|
||||
return (
|
||||
<PanelDataErrorView
|
||||
panelId={id}
|
||||
@ -123,161 +140,46 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<VizLayout.Legend placement={legend.placement}>
|
||||
<VizLegend placement={legend.placement} items={items} displayMode={legend.displayMode} />
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <PlotLegend data={[info.legend]} config={config} maxHeight="35%" maxWidth="60%" {...options.legend} />;
|
||||
};
|
||||
|
||||
const rawValue = (seriesIdx: number, valueIdx: number) => {
|
||||
return frame0Ref.current!.fields[seriesIdx].values[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[valueIdx]).color!;
|
||||
} else {
|
||||
const hasPerBarColor = frame0Ref.current!.fields.some((f) => {
|
||||
const fromThresholds =
|
||||
f.config.custom?.gradientMode === GraphGradientMode.Scheme &&
|
||||
f.config.color?.mode === FieldColorModeId.Thresholds;
|
||||
|
||||
return (
|
||||
fromThresholds ||
|
||||
f.config.mappings?.some((m) => {
|
||||
// ValueToText mappings have a different format, where all of them are grouped into an object keyed by value
|
||||
if (m.type === 'value') {
|
||||
// === MappingType.ValueToText
|
||||
return Object.values(m.options).some((result) => result.color != null);
|
||||
}
|
||||
return m.options.result.color != null;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (hasPerBarColor) {
|
||||
// use opacity from first numeric field
|
||||
let opacityField = frame0Ref.current!.fields.find((f) => f.type === FieldType.number)!;
|
||||
|
||||
fillOpacity = (opacityField.config.custom.fillOpacity ?? 100) / 100;
|
||||
|
||||
getColor = (seriesIdx: number, valueIdx: number) => {
|
||||
let field = frame0Ref.current!.fields[seriesIdx];
|
||||
return field.display!(field.values[valueIdx]).color!;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const {
|
||||
barWidth,
|
||||
barRadius = 0,
|
||||
showValue,
|
||||
groupWidth,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text,
|
||||
xTickLabelRotation,
|
||||
xTickLabelSpacing,
|
||||
fullHighlight,
|
||||
} = options;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
getTimeRange,
|
||||
timeZone,
|
||||
theme,
|
||||
timeZones: [timeZone],
|
||||
orientation,
|
||||
barWidth,
|
||||
barRadius,
|
||||
showValue,
|
||||
groupWidth,
|
||||
xTickLabelRotation,
|
||||
xTickLabelMaxLength,
|
||||
xTickLabelSpacing,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text,
|
||||
rawValue,
|
||||
getColor,
|
||||
fillOpacity,
|
||||
allFrames: info.viz,
|
||||
fullHighlight,
|
||||
hoverMulti: tooltip.mode === TooltipDisplayMode.Multi,
|
||||
});
|
||||
};
|
||||
const legendComp =
|
||||
legend.showLegend && hasVisibleLegendSeries(builder, info.series!) ? (
|
||||
<BarChartLegend data={info.series!} colorField={info.color} {...legend} />
|
||||
) : null;
|
||||
|
||||
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}
|
||||
replaceVariables={replaceVariables}
|
||||
dataLinkPostProcessor={dataLinkPostProcessor}
|
||||
<VizLayout
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
// legend={<BarChartLegend frame={info.series![0]} colorField={info.color} {...legend} />}
|
||||
legend={legendComp}
|
||||
>
|
||||
{(config) => {
|
||||
if (options.tooltip.mode !== TooltipDisplayMode.None) {
|
||||
return (
|
||||
{(vizWidth, vizHeight) => (
|
||||
<UPlotChart config={builder!} data={plotData} width={vizWidth} height={vizHeight}>
|
||||
{props.options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<TooltipPlugin2
|
||||
config={config}
|
||||
config={builder}
|
||||
maxWidth={options.tooltip.maxWidth}
|
||||
hoverMode={
|
||||
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
|
||||
}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2) => {
|
||||
return (
|
||||
<TimeSeriesTooltip
|
||||
frames={info.viz}
|
||||
seriesFrame={info.aligned}
|
||||
series={vizSeries[0]}
|
||||
_rest={info._rest}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
scrollable={isTooltipScrollable(options.tooltip)}
|
||||
maxHeight={options.tooltip.maxHeight}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
maxWidth={options.tooltip.maxWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
</GraphNG>
|
||||
)}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</VizLayout>
|
||||
);
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -184,7 +184,7 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -340,7 +340,7 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -496,7 +496,7 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -652,7 +652,7 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -808,7 +808,7 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -964,7 +964,7 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
@ -1120,7 +1120,7 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = `
|
||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
||||
"width": 1,
|
||||
},
|
||||
"timeZone": undefined,
|
||||
"timeZone": "browser",
|
||||
"values": [Function],
|
||||
},
|
||||
{
|
||||
|
@ -3,12 +3,10 @@ import {
|
||||
FieldColorModeId,
|
||||
FieldConfigProperty,
|
||||
FieldType,
|
||||
getFieldDisplayName,
|
||||
identityOverrideProcessor,
|
||||
PanelPlugin,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { GraphTransform, GraphThresholdsStyleMode, StackingMode, VisibilityMode } from '@grafana/schema';
|
||||
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
|
||||
|
||||
@ -19,7 +17,6 @@ import { TickSpacingEditor } from './TickSpacingEditor';
|
||||
import { changeToBarChartPanelMigrationHandler } from './migrations';
|
||||
import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen';
|
||||
import { BarChartSuggestionsSupplier } from './suggestions';
|
||||
import { prepareBarChartDisplayValues } from './utils';
|
||||
|
||||
export const plugin = new PanelPlugin<Options, FieldConfig>(BarChartPanel)
|
||||
.setPanelChangeHandler(changeToBarChartPanelMigrationHandler)
|
||||
@ -109,21 +106,13 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(BarChartPanel)
|
||||
commonOptionsBuilder.addHideFrom(builder);
|
||||
},
|
||||
})
|
||||
.setPanelOptions((builder, context) => {
|
||||
const disp = prepareBarChartDisplayValues(context.data, config.theme2, context.options ?? ({} as Options));
|
||||
let xaxisPlaceholder = 'First string or time field';
|
||||
const viz = 'viz' in disp ? disp.viz[0] : undefined;
|
||||
if (viz?.fields?.length) {
|
||||
const first = viz.fields[0];
|
||||
xaxisPlaceholder += ` (${getFieldDisplayName(first, viz)})`;
|
||||
}
|
||||
|
||||
.setPanelOptions((builder) => {
|
||||
builder
|
||||
.addFieldNamePicker({
|
||||
path: 'xField',
|
||||
name: 'X Axis',
|
||||
settings: {
|
||||
placeholderText: xaxisPlaceholder,
|
||||
placeholderText: 'First string or time field',
|
||||
},
|
||||
})
|
||||
.addRadio({
|
||||
|
@ -2,12 +2,11 @@ import { assertIsDefined } from 'test/helpers/asserts';
|
||||
|
||||
import {
|
||||
createTheme,
|
||||
DefaultTimeZone,
|
||||
FieldConfig,
|
||||
FieldType,
|
||||
getDefaultTimeRange,
|
||||
MutableDataFrame,
|
||||
VizOrientation,
|
||||
FieldConfigSource,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
LegendDisplayMode,
|
||||
@ -16,10 +15,16 @@ import {
|
||||
GraphGradientMode,
|
||||
StackingMode,
|
||||
SortOrder,
|
||||
defaultTimeZone,
|
||||
} from '@grafana/schema';
|
||||
|
||||
import { FieldConfig as PanelFieldConfig, Options } from './panelcfg.gen';
|
||||
import { BarChartOptionsEX, prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
|
||||
import { FieldConfig as PanelFieldConfig } from './panelcfg.gen';
|
||||
import { prepSeries, prepConfig, PrepConfigOpts } from './utils';
|
||||
|
||||
const fieldConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
function mockDataFrame() {
|
||||
const df1 = new MutableDataFrame({
|
||||
@ -68,13 +73,16 @@ function mockDataFrame() {
|
||||
state: {},
|
||||
});
|
||||
|
||||
const info = prepareBarChartDisplayValues([df1], createTheme(), {} as Options);
|
||||
df1.fields.forEach((f) => (f.config.custom = f.config.custom ?? {}));
|
||||
df2.fields.forEach((f) => (f.config.custom = f.config.custom ?? {}));
|
||||
|
||||
if (!('aligned' in info)) {
|
||||
const info = prepSeries([df1], fieldConfig, StackingMode.None, createTheme());
|
||||
|
||||
if (info.series.length === 0) {
|
||||
throw new Error('Bar chart not prepared correctly');
|
||||
}
|
||||
|
||||
return info.aligned;
|
||||
return info.series[0];
|
||||
}
|
||||
|
||||
jest.mock('@grafana/data', () => ({
|
||||
@ -84,95 +92,99 @@ jest.mock('@grafana/data', () => ({
|
||||
|
||||
describe('BarChart utils', () => {
|
||||
describe('preparePlotConfigBuilder', () => {
|
||||
const frame = mockDataFrame();
|
||||
|
||||
const config: BarChartOptionsEX = {
|
||||
const config: PrepConfigOpts = {
|
||||
series: [mockDataFrame()],
|
||||
// color?: Field | null;
|
||||
timeZone: defaultTimeZone,
|
||||
theme: createTheme(),
|
||||
orientation: VizOrientation.Auto,
|
||||
groupWidth: 20,
|
||||
barWidth: 2,
|
||||
showValue: VisibilityMode.Always,
|
||||
legend: {
|
||||
displayMode: LegendDisplayMode.List,
|
||||
showLegend: true,
|
||||
placement: 'bottom',
|
||||
calcs: [],
|
||||
|
||||
options: {
|
||||
orientation: VizOrientation.Auto,
|
||||
groupWidth: 20,
|
||||
barWidth: 2,
|
||||
showValue: VisibilityMode.Always,
|
||||
legend: {
|
||||
displayMode: LegendDisplayMode.List,
|
||||
showLegend: true,
|
||||
placement: 'bottom',
|
||||
calcs: [],
|
||||
},
|
||||
xTickLabelRotation: 0,
|
||||
xTickLabelMaxLength: 20,
|
||||
stacking: StackingMode.None,
|
||||
tooltip: {
|
||||
mode: TooltipDisplayMode.None,
|
||||
sort: SortOrder.None,
|
||||
},
|
||||
text: {
|
||||
valueSize: 10,
|
||||
},
|
||||
fullHighlight: false,
|
||||
},
|
||||
xTickLabelRotation: 0,
|
||||
xTickLabelMaxLength: 20,
|
||||
stacking: StackingMode.None,
|
||||
tooltip: {
|
||||
mode: TooltipDisplayMode.None,
|
||||
sort: SortOrder.None,
|
||||
},
|
||||
text: {
|
||||
valueSize: 10,
|
||||
},
|
||||
fullHighlight: false,
|
||||
rawValue: (seriesIdx: number, valueIdx: number) => frame.fields[seriesIdx].values[valueIdx],
|
||||
};
|
||||
|
||||
it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => {
|
||||
const result = preparePlotConfigBuilder({
|
||||
const result = prepConfig({
|
||||
...config,
|
||||
options: {
|
||||
...config.options,
|
||||
orientation: v,
|
||||
},
|
||||
series: [mockDataFrame()],
|
||||
orientation: v,
|
||||
frame: frame!,
|
||||
theme: createTheme(),
|
||||
timeZones: [DefaultTimeZone],
|
||||
getTimeRange: getDefaultTimeRange,
|
||||
allFrames: [frame],
|
||||
}).getConfig();
|
||||
}).builder.getConfig();
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([VisibilityMode.Always, VisibilityMode.Auto])('value visibility', (v) => {
|
||||
expect(
|
||||
preparePlotConfigBuilder({
|
||||
prepConfig({
|
||||
...config,
|
||||
showValue: v,
|
||||
frame: frame!,
|
||||
theme: createTheme(),
|
||||
timeZones: [DefaultTimeZone],
|
||||
getTimeRange: getDefaultTimeRange,
|
||||
allFrames: [frame],
|
||||
}).getConfig()
|
||||
options: {
|
||||
...config.options,
|
||||
showValue: v,
|
||||
},
|
||||
series: [mockDataFrame()],
|
||||
}).builder.getConfig()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([StackingMode.None, StackingMode.Percent, StackingMode.Normal])('stacking', (v) => {
|
||||
expect(
|
||||
preparePlotConfigBuilder({
|
||||
prepConfig({
|
||||
...config,
|
||||
stacking: v,
|
||||
frame: frame!,
|
||||
theme: createTheme(),
|
||||
timeZones: [DefaultTimeZone],
|
||||
getTimeRange: getDefaultTimeRange,
|
||||
allFrames: [frame],
|
||||
}).getConfig()
|
||||
options: {
|
||||
...config.options,
|
||||
stacking: v,
|
||||
},
|
||||
series: [mockDataFrame()],
|
||||
}).builder.getConfig()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareGraphableFrames', () => {
|
||||
it('will warn when there is no frames in the response', () => {
|
||||
const result = prepareBarChartDisplayValues([], createTheme(), { stacking: StackingMode.None } as Options);
|
||||
const warning = assertIsDefined('warn' in result ? result : null);
|
||||
const info = prepSeries([], fieldConfig, StackingMode.None, createTheme());
|
||||
const warning = assertIsDefined('warn' in info ? info : null);
|
||||
|
||||
expect(warning.warn).toEqual('No data in response');
|
||||
});
|
||||
|
||||
it('will warn when there is no data in the response', () => {
|
||||
const result = prepareBarChartDisplayValues(
|
||||
const info = prepSeries(
|
||||
[
|
||||
{
|
||||
length: 0,
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
createTheme(),
|
||||
{ stacking: StackingMode.None } as Options
|
||||
fieldConfig,
|
||||
StackingMode.None,
|
||||
createTheme()
|
||||
);
|
||||
const warning = assertIsDefined('warn' in result ? result : null);
|
||||
const warning = assertIsDefined('warn' in info ? info : null);
|
||||
|
||||
expect(warning.warn).toEqual('No data in response');
|
||||
});
|
||||
@ -184,10 +196,11 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', values: [1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as Options);
|
||||
const warning = assertIsDefined('warn' in result ? result : null);
|
||||
df.fields.forEach((f) => (f.config.custom = f.config.custom ?? {}));
|
||||
|
||||
const info = prepSeries([df], fieldConfig, StackingMode.None, createTheme());
|
||||
const warning = assertIsDefined('warn' in info ? info : null);
|
||||
expect(warning.warn).toEqual('Bar charts requires a string or time field');
|
||||
expect(warning).not.toHaveProperty('viz');
|
||||
});
|
||||
|
||||
it('will warn when there are no numeric fields in the response', () => {
|
||||
@ -197,10 +210,11 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', type: FieldType.boolean, values: [true, true, true, true, true] },
|
||||
],
|
||||
});
|
||||
const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as Options);
|
||||
const warning = assertIsDefined('warn' in result ? result : null);
|
||||
df.fields.forEach((f) => (f.config.custom = f.config.custom ?? {}));
|
||||
|
||||
const info = prepSeries([df], fieldConfig, StackingMode.None, createTheme());
|
||||
const warning = assertIsDefined('warn' in info ? info : null);
|
||||
expect(warning.warn).toEqual('No numeric fields found');
|
||||
expect(warning).not.toHaveProperty('viz');
|
||||
});
|
||||
|
||||
it('will convert NaN and Infinty to nulls', () => {
|
||||
@ -210,10 +224,11 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', values: [-10, NaN, 10, -Infinity, +Infinity] },
|
||||
],
|
||||
});
|
||||
const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as Options);
|
||||
const displayValues = assertIsDefined('viz' in result ? result : null);
|
||||
df.fields.forEach((f) => (f.config.custom = f.config.custom ?? {}));
|
||||
|
||||
const field = displayValues.viz[0].fields[1];
|
||||
const info = prepSeries([df], fieldConfig, StackingMode.None, createTheme());
|
||||
|
||||
const field = info.series[0].fields[1];
|
||||
expect(field.values).toMatchInlineSnapshot(`
|
||||
[
|
||||
-10,
|
||||
@ -223,23 +238,10 @@ describe('BarChart utils', () => {
|
||||
null,
|
||||
]
|
||||
`);
|
||||
|
||||
const displayLegendValuesAsc = assertIsDefined('legend' in result ? result : null).legend;
|
||||
const legendField = displayLegendValuesAsc.fields[1];
|
||||
|
||||
expect(legendField.values).toMatchInlineSnapshot(`
|
||||
[
|
||||
-10,
|
||||
null,
|
||||
10,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove unit from legend values when stacking is percent', () => {
|
||||
const frame = new MutableDataFrame({
|
||||
it('should not apply % unit to series when stacking is percent', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'string', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||
{ name: 'a', values: [-10, 20, 10], state: { calcs: { min: -10 } } },
|
||||
@ -247,15 +249,13 @@ describe('BarChart utils', () => {
|
||||
{ name: 'c', values: [10, 10, 10], state: { calcs: { min: 10 } } },
|
||||
],
|
||||
});
|
||||
df.fields.forEach((f) => (f.config.custom = f.config.custom ?? {}));
|
||||
|
||||
const resultAsc = prepareBarChartDisplayValues([frame], createTheme(), {
|
||||
stacking: StackingMode.Percent,
|
||||
} as Options);
|
||||
const displayLegendValuesAsc = assertIsDefined('legend' in resultAsc ? resultAsc : null).legend;
|
||||
const info = prepSeries([df], fieldConfig, StackingMode.Percent, createTheme());
|
||||
|
||||
expect(displayLegendValuesAsc.fields[0].config.unit).toBeUndefined();
|
||||
expect(displayLegendValuesAsc.fields[1].config.unit).toBeUndefined();
|
||||
expect(displayLegendValuesAsc.fields[2].config.unit).toBeUndefined();
|
||||
expect(info.series[0].fields[0].config.unit).toBeUndefined();
|
||||
expect(info.series[0].fields[1].config.unit).toBeUndefined();
|
||||
expect(info.series[0].fields[2].config.unit).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,96 +1,200 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import uPlot, { Padding } from 'uplot';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
Field,
|
||||
FieldConfigSource,
|
||||
FieldType,
|
||||
GrafanaTheme2,
|
||||
cacheFieldDisplayNames,
|
||||
formattedValueToString,
|
||||
getDisplayProcessor,
|
||||
getFieldColorModeForField,
|
||||
cacheFieldDisplayNames,
|
||||
getFieldSeriesColor,
|
||||
GrafanaTheme2,
|
||||
outerJoinDataFrames,
|
||||
TimeZone,
|
||||
VizOrientation,
|
||||
getFieldDisplayName,
|
||||
} from '@grafana/data';
|
||||
import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames';
|
||||
import { decoupleHideFromState } from '@grafana/data/src/field/fieldState';
|
||||
import {
|
||||
AxisColorMode,
|
||||
AxisPlacement,
|
||||
GraphTransform,
|
||||
FieldColorModeId,
|
||||
GraphGradientMode,
|
||||
GraphThresholdsStyleMode,
|
||||
ScaleDirection,
|
||||
GraphTransform,
|
||||
ScaleDistribution,
|
||||
TimeZone,
|
||||
TooltipDisplayMode,
|
||||
VizOrientation,
|
||||
} from '@grafana/schema';
|
||||
import {
|
||||
FIXED_UNIT,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
StackingMode,
|
||||
VizLegendOptions,
|
||||
} from '@grafana/schema';
|
||||
import { FIXED_UNIT, measureText, UPlotConfigBuilder, UPlotConfigPrepFn, UPLOT_AXIS_FONT_SIZE } from '@grafana/ui';
|
||||
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
|
||||
UPlotConfigBuilder,
|
||||
measureText,
|
||||
} from '@grafana/ui';
|
||||
import { AxisProps, UPLOT_AXIS_FONT_SIZE } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
|
||||
import { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils';
|
||||
import { findField } from 'app/features/dimensions';
|
||||
|
||||
import { setClassicPaletteIdxs } from '../timeseries/utils';
|
||||
|
||||
import { BarsOptions, getConfig } from './bars';
|
||||
import { FieldConfig, Options, defaultFieldConfig } from './panelcfg.gen';
|
||||
import { BarChartDisplayValues, BarChartDisplayWarning } from './types';
|
||||
// import { isLegendOrdered } from './utils';
|
||||
|
||||
interface BarSeries {
|
||||
series: DataFrame[];
|
||||
_rest: Field[];
|
||||
color?: Field | null;
|
||||
warn?: string | null;
|
||||
}
|
||||
|
||||
export function prepSeries(
|
||||
frames: DataFrame[],
|
||||
fieldConfig: FieldConfigSource<any>,
|
||||
stacking: StackingMode,
|
||||
theme: GrafanaTheme2,
|
||||
xFieldName?: string,
|
||||
colorFieldName?: string
|
||||
): BarSeries {
|
||||
if (frames.length === 0 || frames.every((fr) => fr.length === 0)) {
|
||||
return { series: [], _rest: [], warn: 'No data in response' };
|
||||
}
|
||||
|
||||
cacheFieldDisplayNames(frames);
|
||||
decoupleHideFromState(frames, fieldConfig);
|
||||
|
||||
let frame: DataFrame | undefined = { ...frames[0] };
|
||||
|
||||
// auto-sort and/or join on first time field (if any)
|
||||
// TODO: should this always join on the xField (if supplied?)
|
||||
const timeFieldIdx = frame.fields.findIndex((f) => f.type === FieldType.time);
|
||||
|
||||
if (timeFieldIdx >= 0 && frames.length > 1) {
|
||||
frame = outerJoinDataFrames({ frames, keepDisplayNames: true }) ?? frame;
|
||||
}
|
||||
|
||||
const xField =
|
||||
// TODO: use matcher
|
||||
frame.fields.find((field) => field.state?.displayName === xFieldName || field.name === xFieldName) ??
|
||||
frame.fields.find((field) => field.type === FieldType.string) ??
|
||||
frame.fields[timeFieldIdx];
|
||||
|
||||
if (xField != null) {
|
||||
const fields: Field[] = [xField];
|
||||
const _rest: Field[] = [];
|
||||
|
||||
const colorField =
|
||||
colorFieldName == null
|
||||
? undefined
|
||||
: frame.fields.find(
|
||||
// TODO: use matcher
|
||||
(field) => field.state?.displayName === colorFieldName || field.name === colorFieldName
|
||||
);
|
||||
|
||||
frame.fields.forEach((field) => {
|
||||
if (field !== xField) {
|
||||
if (field.type === FieldType.number && !field.config.custom?.hideFrom?.viz) {
|
||||
const field2 = {
|
||||
...field,
|
||||
values: field.values.map((v) => (Number.isFinite(v) ? v : null)),
|
||||
// TODO: stacking should be moved from panel opts to fieldConfig (like TimeSeries) so we dont have to do this
|
||||
config: {
|
||||
...field.config,
|
||||
custom: {
|
||||
...field.config.custom,
|
||||
stacking: {
|
||||
group: '_',
|
||||
mode: stacking,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fields.push(field2);
|
||||
} else {
|
||||
_rest.push(field);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let warn: string | null = null;
|
||||
|
||||
if (fields.length === 1) {
|
||||
warn = 'No numeric fields found';
|
||||
}
|
||||
|
||||
frame.fields = fields;
|
||||
|
||||
const series = [frame];
|
||||
|
||||
setClassicPaletteIdxs(series, theme, 0);
|
||||
|
||||
function getBarCharScaleOrientation(orientation: VizOrientation) {
|
||||
if (orientation === VizOrientation.Vertical) {
|
||||
return {
|
||||
xOri: ScaleOrientation.Horizontal,
|
||||
xDir: ScaleDirection.Right,
|
||||
yOri: ScaleOrientation.Vertical,
|
||||
yDir: ScaleDirection.Up,
|
||||
series,
|
||||
_rest,
|
||||
color: colorField,
|
||||
warn,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
xOri: ScaleOrientation.Vertical,
|
||||
xDir: ScaleDirection.Down,
|
||||
yOri: ScaleOrientation.Horizontal,
|
||||
yDir: ScaleDirection.Right,
|
||||
series: [],
|
||||
_rest: [],
|
||||
color: null,
|
||||
warn: 'Bar charts requires a string or time field',
|
||||
};
|
||||
}
|
||||
|
||||
export interface BarChartOptionsEX extends Options {
|
||||
rawValue: (seriesIdx: number, valueIdx: number) => number | null;
|
||||
getColor?: (seriesIdx: number, valueIdx: number, value: unknown) => string | null;
|
||||
timeZone?: TimeZone;
|
||||
fillOpacity?: number;
|
||||
hoverMulti?: boolean;
|
||||
export interface PrepConfigOpts {
|
||||
series: DataFrame[];
|
||||
color?: Field | null;
|
||||
orientation: VizOrientation;
|
||||
options: Options;
|
||||
timeZone: TimeZone;
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
frame,
|
||||
theme,
|
||||
orientation,
|
||||
showValue,
|
||||
groupWidth,
|
||||
barWidth,
|
||||
barRadius = 0,
|
||||
stacking,
|
||||
text,
|
||||
rawValue,
|
||||
getColor,
|
||||
fillOpacity,
|
||||
allFrames,
|
||||
xTickLabelRotation,
|
||||
xTickLabelMaxLength,
|
||||
xTickLabelSpacing = 0,
|
||||
legend,
|
||||
timeZone,
|
||||
fullHighlight,
|
||||
hoverMulti,
|
||||
}) => {
|
||||
export const prepConfig = ({ series, color, orientation, options, timeZone, theme }: PrepConfigOpts) => {
|
||||
let {
|
||||
showValue,
|
||||
groupWidth,
|
||||
barWidth,
|
||||
barRadius = 0,
|
||||
stacking,
|
||||
text,
|
||||
tooltip,
|
||||
xTickLabelRotation,
|
||||
xTickLabelMaxLength,
|
||||
xTickLabelSpacing = 0,
|
||||
legend,
|
||||
fullHighlight,
|
||||
} = options;
|
||||
// this and color is kept up to date by returned prepData()
|
||||
let frame = series[0];
|
||||
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
const formatters = frame.fields.map((f, i) => {
|
||||
if (stacking === StackingMode.Percent) {
|
||||
return getDisplayProcessor({
|
||||
field: {
|
||||
...f,
|
||||
config: {
|
||||
...f.config,
|
||||
unit: 'percentunit',
|
||||
},
|
||||
},
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
return f.display!;
|
||||
});
|
||||
|
||||
const formatValue = (seriesIdx: number, value: unknown) => {
|
||||
return formattedValueToString(frame.fields[seriesIdx].display!(value));
|
||||
return formattedValueToString(formatters[seriesIdx](value));
|
||||
};
|
||||
|
||||
const formatShortValue = (seriesIdx: number, value: unknown) => {
|
||||
@ -98,7 +202,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
};
|
||||
|
||||
// bar orientation -> x scale orientation & direction
|
||||
const vizOrientation = getBarCharScaleOrientation(orientation);
|
||||
const vizOrientation = getScaleOrientation(orientation);
|
||||
|
||||
// Use bar width when only one field
|
||||
if (frame.fields.length === 2) {
|
||||
@ -106,6 +210,52 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
barWidth = 1;
|
||||
}
|
||||
|
||||
const rawValue = (seriesIdx: number, valueIdx: number) => {
|
||||
return frame.fields[seriesIdx].values[valueIdx];
|
||||
};
|
||||
|
||||
// Color by value
|
||||
let getColor: ((seriesIdx: number, valueIdx: number) => string) | undefined = undefined;
|
||||
|
||||
let fillOpacity = 1;
|
||||
|
||||
if (color != null) {
|
||||
const disp = color.display!;
|
||||
fillOpacity = (color.config.custom.fillOpacity ?? 100) / 100;
|
||||
// gradientMode? ignore?
|
||||
getColor = (seriesIdx: number, valueIdx: number) => disp(color!.values[valueIdx]).color!;
|
||||
} else {
|
||||
const hasPerBarColor = frame.fields.some((f) => {
|
||||
const fromThresholds =
|
||||
f.config.custom?.gradientMode === GraphGradientMode.Scheme &&
|
||||
f.config.color?.mode === FieldColorModeId.Thresholds;
|
||||
|
||||
return (
|
||||
fromThresholds ||
|
||||
f.config.mappings?.some((m) => {
|
||||
// ValueToText mappings have a different format, where all of them are grouped into an object keyed by value
|
||||
if (m.type === 'value') {
|
||||
// === MappingType.ValueToText
|
||||
return Object.values(m.options).some((result) => result.color != null);
|
||||
}
|
||||
return m.options.result.color != null;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (hasPerBarColor) {
|
||||
// use opacity from first numeric field
|
||||
let opacityField = frame.fields.find((f) => f.type === FieldType.number)!;
|
||||
|
||||
fillOpacity = (opacityField.config.custom.fillOpacity ?? 100) / 100;
|
||||
|
||||
getColor = (seriesIdx: number, valueIdx: number) => {
|
||||
let field = frame.fields[seriesIdx];
|
||||
return field.display!(field.values[valueIdx]).color!;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const opts: BarsOptions = {
|
||||
xOri: vizOrientation.xOri,
|
||||
xDir: vizOrientation.xDir,
|
||||
@ -126,7 +276,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'),
|
||||
negY: frame.fields.map((f) => f.config.custom?.transform === GraphTransform.NegativeY),
|
||||
fullHighlight,
|
||||
hoverMulti,
|
||||
hoverMulti: tooltip.mode === TooltipDisplayMode.Multi,
|
||||
};
|
||||
|
||||
const config = getConfig(opts, theme);
|
||||
@ -182,14 +332,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
show: xFieldAxisShow,
|
||||
});
|
||||
|
||||
let seriesIndex = 0;
|
||||
const legendOrdered = isLegendOrdered(legend);
|
||||
// let seriesIndex = 0;
|
||||
// const legendOrdered = isLegendOrdered(legend);
|
||||
|
||||
// iterate the y values
|
||||
for (let i = 1; i < frame.fields.length; i++) {
|
||||
const field = frame.fields[i];
|
||||
|
||||
seriesIndex++;
|
||||
// seriesIndex++;
|
||||
|
||||
const customConfig: FieldConfig = { ...defaultFieldConfig, ...field.config.custom };
|
||||
|
||||
@ -246,14 +396,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
// PlotLegend currently gets unfiltered DataFrame[], so index must be into that field array, not the prepped frame's which we're iterating here
|
||||
dataFrameFieldIndex: {
|
||||
fieldIndex: legendOrdered
|
||||
? i
|
||||
: allFrames[0].fields.findIndex(
|
||||
(f) => f.type === FieldType.number && f.state?.seriesIndex === seriesIndex - 1
|
||||
),
|
||||
frameIndex: 0,
|
||||
},
|
||||
// dataFrameFieldIndex: {
|
||||
// fieldIndex: legendOrdered
|
||||
// ? i
|
||||
// : allFrames[0].fields.findIndex(
|
||||
// (f) => f.type === FieldType.number && f.state?.seriesIndex === seriesIndex - 1
|
||||
// ),
|
||||
// frameIndex: 0,
|
||||
// },
|
||||
});
|
||||
|
||||
// The builder will manage unique scaleKeys and combine where appropriate
|
||||
@ -314,7 +464,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
|
||||
builder.setStackingGroups(stackingGroups);
|
||||
|
||||
return builder;
|
||||
return {
|
||||
builder,
|
||||
prepData: (_series: DataFrame[], _color?: Field | null) => {
|
||||
series = _series;
|
||||
frame = series[0];
|
||||
color = _color;
|
||||
|
||||
return builder.prepData!(series);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function shortenValue(value: string, length: number) {
|
||||
@ -373,167 +532,20 @@ function getRotationPadding(
|
||||
];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function prepareBarChartDisplayValues(
|
||||
series: DataFrame[],
|
||||
theme: GrafanaTheme2,
|
||||
options: Options
|
||||
): BarChartDisplayValues | BarChartDisplayWarning {
|
||||
if (!series.length || series.every((fr) => fr.length === 0)) {
|
||||
return { warn: 'No data in response' };
|
||||
}
|
||||
|
||||
cacheFieldDisplayNames(series);
|
||||
|
||||
// Bar chart requires a single frame
|
||||
const frame =
|
||||
series.length === 1
|
||||
? maybeSortFrame(
|
||||
series[0],
|
||||
series[0].fields.findIndex((f) => f.type === FieldType.time)
|
||||
)
|
||||
: outerJoinDataFrames({ frames: series, keepDisplayNames: true });
|
||||
|
||||
if (!frame) {
|
||||
return { warn: 'Unable to join data' };
|
||||
}
|
||||
|
||||
// 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' };
|
||||
}
|
||||
}
|
||||
|
||||
let xField: Field | undefined = undefined;
|
||||
if (options.xField) {
|
||||
xField = findField(frame, options.xField);
|
||||
if (!xField) {
|
||||
return { warn: 'Configured x field not found' };
|
||||
}
|
||||
}
|
||||
|
||||
let stringField: Field | undefined = undefined;
|
||||
let timeField: Field | undefined = undefined;
|
||||
let fields: Field[] = [];
|
||||
for (const field of frame.fields) {
|
||||
if (field === xField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case FieldType.string:
|
||||
if (!stringField) {
|
||||
stringField = field;
|
||||
}
|
||||
break;
|
||||
|
||||
case FieldType.time:
|
||||
if (!timeField) {
|
||||
timeField = field;
|
||||
}
|
||||
break;
|
||||
|
||||
case FieldType.number: {
|
||||
const copy = {
|
||||
...field,
|
||||
state: {
|
||||
...field.state,
|
||||
seriesIndex: fields.length, // off by one?
|
||||
},
|
||||
config: {
|
||||
...field.config,
|
||||
custom: {
|
||||
...field.config.custom,
|
||||
stacking: {
|
||||
group: '_',
|
||||
mode: options.stacking,
|
||||
},
|
||||
},
|
||||
},
|
||||
values: field.values.map((v) => {
|
||||
if (!(Number.isFinite(v) || v == null)) {
|
||||
return null;
|
||||
}
|
||||
return v;
|
||||
}),
|
||||
};
|
||||
|
||||
if (options.stacking === StackingMode.Percent) {
|
||||
copy.config.unit = 'percentunit';
|
||||
copy.display = getDisplayProcessor({ field: copy, theme });
|
||||
}
|
||||
|
||||
fields.push(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let firstField = xField;
|
||||
if (!firstField) {
|
||||
firstField = stringField || timeField;
|
||||
}
|
||||
|
||||
if (!firstField) {
|
||||
function getScaleOrientation(orientation: VizOrientation) {
|
||||
if (orientation === VizOrientation.Vertical) {
|
||||
return {
|
||||
warn: 'Bar charts requires a string or time field',
|
||||
xOri: ScaleOrientation.Horizontal,
|
||||
xDir: ScaleDirection.Right,
|
||||
yOri: ScaleOrientation.Vertical,
|
||||
yDir: ScaleDirection.Up,
|
||||
};
|
||||
}
|
||||
|
||||
// if both string and time fields exist, remove unused leftover time field
|
||||
if (frame.fields[0].type === FieldType.time && frame.fields[0] !== firstField) {
|
||||
frame.fields.shift();
|
||||
}
|
||||
|
||||
setClassicPaletteIdxs([frame], theme, 0);
|
||||
|
||||
if (!fields.length) {
|
||||
return {
|
||||
warn: 'No numeric fields found',
|
||||
};
|
||||
}
|
||||
|
||||
// Show the first number value
|
||||
if (colorByField && fields.length > 1) {
|
||||
const firstNumber = fields.find((f) => f !== colorByField);
|
||||
if (firstNumber) {
|
||||
fields = [firstNumber];
|
||||
}
|
||||
}
|
||||
|
||||
// If stacking is percent, we need to correct the legend fields unit and display
|
||||
let legendFields: Field[] = cloneDeep(fields);
|
||||
if (options.stacking === StackingMode.Percent) {
|
||||
legendFields.map((field) => {
|
||||
const alignedFrameField = frame.fields.find(
|
||||
(f) => getFieldDisplayName(f, frame) === getFieldDisplayName(f, frame)
|
||||
);
|
||||
|
||||
field.config.unit = alignedFrameField?.config?.unit ?? undefined;
|
||||
field.display = getDisplayProcessor({ field: field, theme });
|
||||
});
|
||||
}
|
||||
|
||||
// String field is first, make sure fields / legend fields indexes match
|
||||
fields.unshift(firstField);
|
||||
legendFields.unshift(firstField);
|
||||
|
||||
return {
|
||||
aligned: frame,
|
||||
colorByField,
|
||||
viz: [
|
||||
{
|
||||
fields: fields, // ideally: fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.viz)),
|
||||
length: firstField.values.length,
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
fields: legendFields,
|
||||
length: firstField.values.length,
|
||||
},
|
||||
xOri: ScaleOrientation.Vertical,
|
||||
xDir: ScaleDirection.Down,
|
||||
yOri: ScaleOrientation.Horizontal,
|
||||
yDir: ScaleDirection.Right,
|
||||
};
|
||||
}
|
||||
|
||||
export const isLegendOrdered = (options: VizLegendOptions) => Boolean(options?.sortBy && options.sortDesc !== null);
|
||||
|
@ -299,8 +299,7 @@ export const CandlestickPanel = ({
|
||||
|
||||
return (
|
||||
<TimeSeriesTooltip
|
||||
frames={[info.frame]}
|
||||
seriesFrame={alignedFrame}
|
||||
series={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
|
@ -115,8 +115,7 @@ export const StateTimelinePanel = ({
|
||||
|
||||
return (
|
||||
<StateTimelineTooltip2
|
||||
frames={frames ?? []}
|
||||
seriesFrame={alignedFrame}
|
||||
series={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { FieldType, getFieldDisplayName, TimeRange } from '@grafana/data';
|
||||
import { FieldType, TimeRange } from '@grafana/data';
|
||||
import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen';
|
||||
import { TooltipDisplayMode, useStyles2 } from '@grafana/ui';
|
||||
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||
@ -19,8 +19,7 @@ interface StateTimelineTooltip2Props extends TimeSeriesTooltipProps {
|
||||
}
|
||||
|
||||
export const StateTimelineTooltip2 = ({
|
||||
frames,
|
||||
seriesFrame,
|
||||
series,
|
||||
dataIdxs,
|
||||
seriesIdx,
|
||||
mode = TooltipDisplayMode.Single,
|
||||
@ -34,7 +33,7 @@ export const StateTimelineTooltip2 = ({
|
||||
}: StateTimelineTooltip2Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const xField = seriesFrame.fields[0];
|
||||
const xField = series.fields[0];
|
||||
|
||||
const dataIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null);
|
||||
|
||||
@ -42,11 +41,11 @@ export const StateTimelineTooltip2 = ({
|
||||
|
||||
mode = isPinned ? TooltipDisplayMode.Single : mode;
|
||||
|
||||
const contentItems = getContentItems(seriesFrame.fields, xField, dataIdxs, seriesIdx, mode, sortOrder);
|
||||
const contentItems = getContentItems(series.fields, xField, dataIdxs, seriesIdx, mode, sortOrder);
|
||||
|
||||
// append duration in single mode
|
||||
if (withDuration && mode === TooltipDisplayMode.Single) {
|
||||
const field = seriesFrame.fields[seriesIdx!];
|
||||
const field = series.fields[seriesIdx!];
|
||||
const nextStateIdx = findNextStateIndex(field, dataIdx!);
|
||||
let nextStateTs;
|
||||
if (nextStateIdx) {
|
||||
@ -69,7 +68,7 @@ export const StateTimelineTooltip2 = ({
|
||||
let footer: ReactNode;
|
||||
|
||||
if (isPinned && seriesIdx != null) {
|
||||
const field = seriesFrame.fields[seriesIdx];
|
||||
const field = series.fields[seriesIdx];
|
||||
const dataIdx = dataIdxs[seriesIdx]!;
|
||||
const links = getDataLinks(field, dataIdx);
|
||||
|
||||
@ -77,7 +76,7 @@ export const StateTimelineTooltip2 = ({
|
||||
}
|
||||
|
||||
const headerItem: VizTooltipItem = {
|
||||
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
||||
label: xField.type === FieldType.time ? '' : xField.state?.displayName ?? xField.name,
|
||||
value: xVal,
|
||||
};
|
||||
|
||||
|
@ -120,8 +120,7 @@ export const StatusHistoryPanel = ({
|
||||
|
||||
return (
|
||||
<StateTimelineTooltip2
|
||||
frames={frames ?? []}
|
||||
seriesFrame={alignedFrame}
|
||||
series={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
|
@ -123,8 +123,7 @@ export const TimeSeriesPanel = ({
|
||||
return (
|
||||
// not sure it header time here works for annotations, since it's taken from nearest datapoint index
|
||||
<TimeSeriesTooltip
|
||||
frames={frames}
|
||||
seriesFrame={alignedFrame}
|
||||
series={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { DataFrame, FieldType, getFieldDisplayName } from '@grafana/data';
|
||||
import { DataFrame, Field, FieldType } from '@grafana/data';
|
||||
import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||
@ -11,14 +11,18 @@ import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types';
|
||||
import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils';
|
||||
|
||||
import { getDataLinks } from '../status-history/utils';
|
||||
import { fmt } from '../xychart/utils';
|
||||
|
||||
// exemplar / annotation / time region hovering?
|
||||
// add annotation UI / alert dismiss UI?
|
||||
|
||||
export interface TimeSeriesTooltipProps {
|
||||
frames?: DataFrame[];
|
||||
// aligned series frame
|
||||
seriesFrame: DataFrame;
|
||||
series: DataFrame;
|
||||
|
||||
// aligned fields that are not series
|
||||
_rest?: Field[];
|
||||
|
||||
// hovered points
|
||||
dataIdxs: Array<number | null>;
|
||||
// closest/hovered series
|
||||
@ -34,8 +38,8 @@ export interface TimeSeriesTooltipProps {
|
||||
}
|
||||
|
||||
export const TimeSeriesTooltip = ({
|
||||
frames,
|
||||
seriesFrame,
|
||||
series,
|
||||
_rest,
|
||||
dataIdxs,
|
||||
seriesIdx,
|
||||
mode = TooltipDisplayMode.Single,
|
||||
@ -47,12 +51,12 @@ export const TimeSeriesTooltip = ({
|
||||
}: TimeSeriesTooltipProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const xField = seriesFrame.fields[0];
|
||||
const xField = series.fields[0];
|
||||
|
||||
const xVal = xField.display!(xField.values[dataIdxs[0]!]).text;
|
||||
|
||||
const contentItems = getContentItems(
|
||||
seriesFrame.fields,
|
||||
series.fields,
|
||||
xField,
|
||||
dataIdxs,
|
||||
seriesIdx,
|
||||
@ -61,24 +65,35 @@ export const TimeSeriesTooltip = ({
|
||||
(field) => field.type === FieldType.number || field.type === FieldType.enum
|
||||
);
|
||||
|
||||
_rest?.forEach((field) => {
|
||||
if (!field.config.custom?.hideFrom?.tooltip) {
|
||||
contentItems.push({
|
||||
label: field.state?.displayName ?? field.name,
|
||||
value: fmt(field, field.values[dataIdxs[0]!]),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let footer: ReactNode;
|
||||
|
||||
if (isPinned && seriesIdx != null) {
|
||||
const field = seriesFrame.fields[seriesIdx];
|
||||
const field = series.fields[seriesIdx];
|
||||
const dataIdx = dataIdxs[seriesIdx]!;
|
||||
const links = getDataLinks(field, dataIdx);
|
||||
|
||||
footer = <VizTooltipFooter dataLinks={links} annotate={annotate} />;
|
||||
}
|
||||
|
||||
const headerItem: VizTooltipItem = {
|
||||
label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames),
|
||||
value: xVal,
|
||||
};
|
||||
const headerItem: VizTooltipItem | null = xField.config.custom?.hideFrom?.tooltip
|
||||
? null
|
||||
: {
|
||||
label: xField.type === FieldType.time ? '' : xField.state?.displayName ?? xField.name,
|
||||
value: xVal,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VizTooltipHeader item={headerItem} isPinned={isPinned} />
|
||||
{headerItem != null && <VizTooltipHeader item={headerItem} isPinned={isPinned} />}
|
||||
<VizTooltipContent items={contentItems} isPinned={isPinned} scrollable={scrollable} maxHeight={maxHeight} />
|
||||
{footer}
|
||||
</div>
|
||||
|
@ -85,7 +85,7 @@ export const AnnotationMarker2 = ({
|
||||
>
|
||||
{contents &&
|
||||
createPortal(
|
||||
<div ref={refs.setFloating} className={styles.annoBox} style={floatingStyles}>
|
||||
<div ref={refs.setFloating} className={styles.annoBox} style={floatingStyles} data-testid="annotation-marker">
|
||||
{contents}
|
||||
</div>,
|
||||
portalRoot
|
||||
|
@ -123,8 +123,7 @@ export const TrendPanel = ({
|
||||
render={(u, dataIdxs, seriesIdx, isPinned = false) => {
|
||||
return (
|
||||
<TimeSeriesTooltip
|
||||
frames={info.frames!}
|
||||
seriesFrame={alignedDataFrame}
|
||||
series={alignedDataFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
|
Loading…
Reference in New Issue
Block a user