diff --git a/.betterer.results b/.betterer.results index ea9e7b762e6..c78641294f7 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/devenv/dev-dashboards/panel-barchart/barchart-tooltips-legends.json b/devenv/dev-dashboards/panel-barchart/barchart-tooltips-legends.json index 70deb1af0df..6a0046a0ed9 100644 --- a/devenv/dev-dashboards/panel-barchart/barchart-tooltips-legends.json +++ b/devenv/dev-dashboards/panel-barchart/barchart-tooltips-legends.json @@ -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": "" } \ No newline at end of file diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 0d8058fc531..7848983c7bf 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -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, }); diff --git a/public/app/plugins/panel/barchart/BarChartLegend.tsx b/public/app/plugins/panel/barchart/BarChartLegend.tsx new file mode 100644 index 00000000000..a5230e096d0 --- /dev/null +++ b/public/app/plugins/panel/barchart/BarChartLegend.tsx @@ -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 { + 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 ( + + + + ); + } + } + + 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 ( + + + + ); + } +); + +BarChartLegend.displayName = 'BarChartLegend'; diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 37a7bfa1013..3cff8d7dabc 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -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 {} +const charWidth = measureText('M', UPLOT_AXIS_FONT_SIZE).width; +const toRads = Math.PI / 180; -const propsToDiff: Array = [ - 'orientation', - 'barWidth', - 'barRadius', - 'xTickLabelRotation', - 'xTickLabelMaxLength', - 'xTickLabelSpacing', - 'groupWidth', - 'stacking', - 'showValue', - 'xField', - 'colorField', - 'legend', - (prev: BarChartProps, next: BarChartProps) => next.text?.valueSize === prev.text?.valueSize, -]; +export const BarChartPanel = (props: PanelProps) => { + const { + data, + options, + fieldConfig, + width, + height, + timeZone, + id, + // replaceVariables + } = props; -interface Props extends PanelProps {} + // 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(); - const colorByFieldRef = useRef(); + 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 ( { - const { legend } = options; - - if (!config || legend.showLegend === false) { - return null; - } - - if (info.colorByField) { - const items = getFieldLegendItem([info.colorByField], theme); - if (items?.length) { - return ( - - - - ); - } - } - - return ; - }; - - const rawValue = (seriesIdx: number, valueIdx: number) => { - return frame0Ref.current!.fields[seriesIdx].values[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!) ? ( + + ) : null; return ( - f[0]} // already processed in by the panel above! - renderLegend={renderLegend} - legend={options.legend} - timeZone={timeZone} - timeRange={{ from: 1, to: 1 } as unknown as TimeRange} // HACK - structureRev={structureRev} - width={width} - height={height} - replaceVariables={replaceVariables} - dataLinkPostProcessor={dataLinkPostProcessor} + } + legend={legendComp} > - {(config) => { - if (options.tooltip.mode !== TooltipDisplayMode.None) { - return ( + {(vizWidth, vizHeight) => ( + + {props.options.tooltip.mode !== TooltipDisplayMode.None && ( { return ( ); }} - maxWidth={options.tooltip.maxWidth} /> - ); - } - - return null; - }} - + )} + + )} + ); }; diff --git a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap index 460a05d35ca..f11dd523c32 100644 --- a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap +++ b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap @@ -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], }, { diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 6fb9254af22..005ba87124d 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -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(BarChartPanel) .setPanelChangeHandler(changeToBarChartPanelMigrationHandler) @@ -109,21 +106,13 @@ export const plugin = new PanelPlugin(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({ diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index 07ea9e55e24..606d0e41229 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -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(); }); }); }); diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 3c466c40376..550ea52c711 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -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, + 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 = ({ - 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 = ({ }; // 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 = ({ 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 = ({ 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 = ({ 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 = ({ // 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 = ({ 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); diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 5c535d61fd9..3e18162e13c 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -299,8 +299,7 @@ export const CandlestickPanel = ({ return ( { 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, }; diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 2d607b3b935..c919b183c0e 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -120,8 +120,7 @@ export const StatusHistoryPanel = ({ return ( ; // 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 = ; } - 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 (
- + {headerItem != null && } {footer}
diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx index 0931792464c..2353971f734 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx @@ -85,7 +85,7 @@ export const AnnotationMarker2 = ({ > {contents && createPortal( -
+
{contents}
, portalRoot diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 04a23cf564d..2242125429f 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -123,8 +123,7 @@ export const TrendPanel = ({ render={(u, dataIdxs, seriesIdx, isPinned = false) => { return (