From d5e35c4b7816aafc44e34972ea48aa2fa5d8c581 Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Fri, 27 Sep 2024 17:02:03 +0200 Subject: [PATCH] Legend: Render legend threshold colors (#92838) * feat(barchart): render legend threshold and value mapping colors Co-authored-by: Leon Sorokin --- .../src/components/VizLegend/VizLegend.tsx | 42 ++++++++--- .../src/components/VizLegend/types.ts | 2 + .../core/components/TimelineChart/utils.ts | 70 ++++++++++++++++++- .../plugins/panel/barchart/BarChartLegend.tsx | 66 ++++++++++++++--- public/app/plugins/panel/barchart/utils.ts | 5 +- 5 files changed, 158 insertions(+), 27 deletions(-) diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx index 789a90a1087..f07c3abff22 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx @@ -16,6 +16,8 @@ import { mapMouseEventToMode } from './utils'; */ export function VizLegend({ items, + thresholdItems, + mappingItems, displayMode, sortBy: sortKey, seriesVisibilityChangeBehavior = SeriesVisibilityChangeBehavior.Isolate, @@ -83,6 +85,24 @@ export function VizLegend({ [onToggleSeriesVisibility, onLabelClick, seriesVisibilityChangeBehavior] ); + const makeVizLegendList = useCallback( + (items: VizLegendItem[]) => { + return ( + + className={className} + placement={placement} + onLabelMouseOver={onMouseOver} + onLabelMouseOut={onMouseOut} + onLabelClick={onLegendLabelClick} + itemRenderer={itemRenderer} + readonly={readonly} + items={items} + /> + ); + }, + [className, placement, onMouseOver, onMouseOut, onLegendLabelClick, itemRenderer, readonly] + ); + switch (displayMode) { case LegendDisplayMode.Table: return ( @@ -102,17 +122,19 @@ export function VizLegend({ /> ); case LegendDisplayMode.List: + const isThresholdsEnabled = thresholdItems && thresholdItems.length > 1; + const isValueMappingEnabled = mappingItems && mappingItems.length > 0; return ( - - className={className} - items={items} - placement={placement} - onLabelMouseOver={onMouseOver} - onLabelMouseOut={onMouseOut} - onLabelClick={onLegendLabelClick} - itemRenderer={itemRenderer} - readonly={readonly} - /> + <> + {/* render items when single series and there is no thresholds and no value mappings + * render items when multi series and there is no thresholds + */} + {!isThresholdsEnabled && (!isValueMappingEnabled || items.length > 1) && makeVizLegendList(items)} + {/* render threshold colors if From thresholds scheme selected */} + {isThresholdsEnabled && makeVizLegendList(thresholdItems)} + {/* render value mapping colors */} + {isValueMappingEnabled && makeVizLegendList(mappingItems)} + ); default: return null; diff --git a/packages/grafana-ui/src/components/VizLegend/types.ts b/packages/grafana-ui/src/components/VizLegend/types.ts index cf718a64756..4032276bf6a 100644 --- a/packages/grafana-ui/src/components/VizLegend/types.ts +++ b/packages/grafana-ui/src/components/VizLegend/types.ts @@ -12,6 +12,8 @@ export interface VizLegendBaseProps { placement: LegendPlacement; className?: string; items: Array>; + thresholdItems?: Array>; + mappingItems?: Array>; seriesVisibilityChangeBehavior?: SeriesVisibilityChangeBehavior; onLabelClick?: (item: VizLegendItem, event: React.MouseEvent) => void; itemRenderer?: (item: VizLegendItem, index: number) => JSX.Element; diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 98954d8308b..26f1227ebe0 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -16,6 +16,8 @@ import { TimeRange, cacheFieldDisplayNames, outerJoinDataFrames, + ValueMapping, + ThresholdsConfig, } from '@grafana/data'; import { maybeSortFrame, NULL_RETAIN } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; @@ -453,9 +455,13 @@ export function makeFramePerSeries(frames: DataFrame[]) { return outFrames; } -export function getThresholdItems(fieldConfig: FieldConfig, theme: GrafanaTheme2): VizLegendItem[] { +export function getThresholdItems( + fieldConfig: FieldConfig, + theme: GrafanaTheme2, + thresholdItems?: ThresholdsConfig +): VizLegendItem[] { const items: VizLegendItem[] = []; - const thresholds = fieldConfig.thresholds; + const thresholds = thresholdItems ? thresholdItems : fieldConfig.thresholds; if (!thresholds || !thresholds.steps.length) { return items; } @@ -491,6 +497,66 @@ export function getThresholdItems(fieldConfig: FieldConfig, theme: GrafanaTheme2 return items; } +export function getValueMappingItems(mappings: ValueMapping[], theme: GrafanaTheme2): VizLegendItem[] { + const items: VizLegendItem[] = []; + if (!mappings) { + return items; + } + + for (let mapping of mappings) { + const { options, type } = mapping; + + if (type === MappingType.ValueToText) { + for (let [label, value] of Object.entries(options)) { + const color = value.color; + items.push({ + label: label, + color: theme.visualization.getColorByName(color ?? FALLBACK_COLOR), + yAxis: 1, + }); + } + } + + if (type === MappingType.RangeToText) { + const { from, result, to } = options; + const { text, color } = result; + const label = text ? `[${from} - ${to}] ${text}` : `[${from} - ${to}]`; + + items.push({ + label: label, + color: theme.visualization.getColorByName(color ?? FALLBACK_COLOR), + yAxis: 1, + }); + } + + if (type === MappingType.RegexToText) { + const { pattern, result } = options; + const { text, color } = result; + const label = `${text || pattern}`; + + items.push({ + label: label, + color: theme.visualization.getColorByName(color ?? FALLBACK_COLOR), + yAxis: 1, + }); + } + + if (type === MappingType.SpecialValue) { + const { match, result } = options; + const { text, color } = result; + const label = `${text || match}`; + + items.push({ + label: label, + color: theme.visualization.getColorByName(color ?? FALLBACK_COLOR), + yAxis: 1, + }); + } + } + + return items; +} + export function prepareTimelineLegendItems( frames: DataFrame[] | undefined, options: VizLegendOptions, diff --git a/public/app/plugins/panel/barchart/BarChartLegend.tsx b/public/app/plugins/panel/barchart/BarChartLegend.tsx index dd13d2301ba..d65579dec7a 100644 --- a/public/app/plugins/panel/barchart/BarChartLegend.tsx +++ b/public/app/plugins/panel/barchart/BarChartLegend.tsx @@ -1,11 +1,19 @@ +import { includes } from 'lodash'; import { memo } from 'react'; -import { DataFrame, Field, getFieldSeriesColor } from '@grafana/data'; +import { + DataFrame, + Field, + FieldColorModeId, + getFieldSeriesColor, + ThresholdsConfig, + ThresholdsMode, + ValueMapping, +} 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'; - +import { getThresholdItems, getValueMappingItems } from 'app/core/components/TimelineChart/utils'; interface BarChartLegend2Props extends VizLegendOptions, Omit { data: DataFrame[]; colorField?: Field | null; @@ -32,17 +40,51 @@ export const BarChartLegend = memo( ({ data, placement, calcs, displayMode, colorField, ...vizLayoutLegendProps }: BarChartLegend2Props) => { const theme = useTheme2(); - if (colorField != null) { - const items = getFieldLegendItem([colorField], theme); + const fieldConfig = data[0].fields[0].config; + const colorMode = fieldConfig.color?.mode; - if (items?.length) { - return ( - - - - ); + const thresholdItems: VizLegendItem[] = []; + if (colorMode === FieldColorModeId.Thresholds) { + const thresholdsAbsolute: ThresholdsConfig = { mode: ThresholdsMode.Absolute, steps: [] }; + const thresholdsPercent: ThresholdsConfig = { mode: ThresholdsMode.Percentage, steps: [] }; + + for (let i = 1; i < data[0].fields.length; i++) { + const field = data[0].fields[i]; + // there is no reason to add threshold with only one (Base) step + if (field.config.thresholds && field.config.thresholds.steps.length > 1) { + if (field.config.thresholds.mode === ThresholdsMode.Absolute) { + for (const step of field.config.thresholds.steps) { + if (!includes(thresholdsAbsolute.steps, step)) { + thresholdsAbsolute.steps.push(step); + } + } + } else { + for (const step of field.config.thresholds.steps) { + if (!includes(thresholdsPercent.steps, step)) { + thresholdsPercent.steps.push(step); + } + } + } + } + } + + const thresholdAbsoluteItems: VizLegendItem[] = getThresholdItems(fieldConfig, theme, thresholdsAbsolute); + const thresholdPercentItems: VizLegendItem[] = getThresholdItems(fieldConfig, theme, thresholdsPercent); + thresholdItems.push(...thresholdAbsoluteItems, ...thresholdPercentItems); + } + + const valueMappings: ValueMapping[] = []; + for (let i = 1; i < data[0].fields.length; i++) { + const mappings = data[0].fields[i].config.mappings; + if (mappings) { + for (const mapping of mappings) { + if (!includes(valueMappings, mapping)) { + valueMappings.push(mapping); + } + } } } + const valueMappingItems: VizLegendItem[] = getValueMappingItems(valueMappings, theme); const legendItems = data[0].fields .slice(1) @@ -80,6 +122,8 @@ export const BarChartLegend = memo( 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; + const fromThresholds = f.config.color?.mode === FieldColorModeId.Thresholds; return ( fromThresholds ||