Legend: Render legend threshold colors (#92838)

* feat(barchart): render legend threshold and value mapping colors

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Ihor Yeromin
2024-09-27 17:02:03 +02:00
committed by GitHub
parent c46736f490
commit d5e35c4b78
5 changed files with 158 additions and 27 deletions

View File

@@ -16,6 +16,8 @@ import { mapMouseEventToMode } from './utils';
*/
export function VizLegend<T>({
items,
thresholdItems,
mappingItems,
displayMode,
sortBy: sortKey,
seriesVisibilityChangeBehavior = SeriesVisibilityChangeBehavior.Isolate,
@@ -83,6 +85,24 @@ export function VizLegend<T>({
[onToggleSeriesVisibility, onLabelClick, seriesVisibilityChangeBehavior]
);
const makeVizLegendList = useCallback(
(items: VizLegendItem[]) => {
return (
<VizLegendList<T>
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<T>({
/>
);
case LegendDisplayMode.List:
const isThresholdsEnabled = thresholdItems && thresholdItems.length > 1;
const isValueMappingEnabled = mappingItems && mappingItems.length > 0;
return (
<VizLegendList<T>
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;

View File

@@ -12,6 +12,8 @@ export interface VizLegendBaseProps<T> {
placement: LegendPlacement;
className?: string;
items: Array<VizLegendItem<T>>;
thresholdItems?: Array<VizLegendItem<T>>;
mappingItems?: Array<VizLegendItem<T>>;
seriesVisibilityChangeBehavior?: SeriesVisibilityChangeBehavior;
onLabelClick?: (item: VizLegendItem<T>, event: React.MouseEvent<HTMLButtonElement>) => void;
itemRenderer?: (item: VizLegendItem<T>, index: number) => JSX.Element;

View File

@@ -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,

View File

@@ -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<VizLayoutLegendProps, 'children'> {
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 (
<VizLayout.Legend placement={placement}>
<VizLegend placement={placement} items={items} displayMode={displayMode} />
</VizLayout.Legend>
);
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(
<VizLegend
placement={placement}
items={legendItems}
thresholdItems={thresholdItems}
mappingItems={valueMappingItems}
displayMode={displayMode}
sortBy={vizLayoutLegendProps.sortBy}
sortDesc={vizLayoutLegendProps.sortDesc}

View File

@@ -18,7 +18,6 @@ import {
AxisColorMode,
AxisPlacement,
FieldColorModeId,
GraphGradientMode,
GraphThresholdsStyleMode,
GraphTransform,
ScaleDistribution,
@@ -230,9 +229,7 @@ export const prepConfig = ({ series, totalSeries, color, orientation, options, t
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;
const fromThresholds = f.config.color?.mode === FieldColorModeId.Thresholds;
return (
fromThresholds ||