Stat: Add percent change color modes (#88205)

* Stat: Add percent change color modes

* Apply color mode to percent change component

* Remove todo

* Create percent change color function and add tests

* drew08t/stat-percent-change-color-modes/ maybe fix import error?

---------

Co-authored-by: jev forsberg <jev.forsberg@grafana.com>
This commit is contained in:
Drew Slobodnjak 2024-05-24 14:45:02 -07:00 committed by GitHub
parent ed42119907
commit 14957953db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 158 additions and 18 deletions

View File

@ -552,6 +552,15 @@ export enum BigValueTextMode {
ValueAndName = 'value_and_name',
}
/**
* TODO docs
*/
export enum PercentChangeColorMode {
Inverted = 'inverted',
SameAsValue = 'same_as_value',
Standard = 'standard',
}
/**
* TODO -- should not be table specific!
* TODO docs

View File

@ -190,6 +190,9 @@ BigValueJustifyMode: "auto" | "center" @cuetsy(kind="enum")
// TODO docs
BigValueTextMode: "auto" | "value" | "value_and_name" | "name" | "none" @cuetsy(kind="enum",memberNames="Auto|Value|ValueAndName|Name|None")
// TODO docs
PercentChangeColorMode: "standard" | "inverted" | "same_as_value" @cuetsy(kind="enum",memberNames="Standard|Inverted|SameAsValue")
// TODO -- should not be table specific!
// TODO docs
FieldTextAlignment: "auto" | "left" | "right" | "center" @cuetsy(kind="type")

View File

@ -16,6 +16,7 @@ export interface Options extends common.SingleStatBaseOptions {
colorMode: common.BigValueColorMode;
graphMode: common.BigValueGraphMode;
justifyMode: common.BigValueJustifyMode;
percentChangeColorMode: common.PercentChangeColorMode;
showPercentChange: boolean;
textMode: common.BigValueTextMode;
wideLayout: boolean;
@ -25,6 +26,7 @@ export const defaultOptions: Partial<Options> = {
colorMode: common.BigValueColorMode.Value,
graphMode: common.BigValueGraphMode.Area,
justifyMode: common.BigValueJustifyMode.Auto,
percentChangeColorMode: common.PercentChangeColorMode.Standard,
showPercentChange: false,
textMode: common.BigValueTextMode.Auto,
wideLayout: true,

View File

@ -2,7 +2,7 @@ import { cx } from '@emotion/css';
import React, { PureComponent } from 'react';
import { DisplayValue, DisplayValueAlignmentFactors, FieldSparkline } from '@grafana/data';
import { VizTextDisplayOptions } from '@grafana/schema';
import { PercentChangeColorMode, VizTextDisplayOptions } from '@grafana/schema';
import { Themeable2 } from '../../types';
import { clearButtonStyles } from '../Button';
@ -67,6 +67,8 @@ export interface Props extends Themeable2 {
textMode?: BigValueTextMode;
/** If true disables the tooltip */
hasLinks?: boolean;
/** Percent change color mode */
percentChangeColorMode?: PercentChangeColorMode;
/**
* If part of a series of stat panes, this is the total number.
@ -94,6 +96,7 @@ export class BigValue extends PureComponent<Props> {
const titleStyles = layout.getTitleStyles();
const textValues = layout.textValues;
const percentChange = this.props.value.percentChange;
const percentChangeColorMode = this.props.percentChangeColorMode;
const showPercentChange = percentChange != null && !Number.isNaN(percentChange);
// When there is an outer data link this tooltip will override the outer native tooltip
@ -106,7 +109,10 @@ export class BigValue extends PureComponent<Props> {
{textValues.title && <div style={titleStyles}>{textValues.title}</div>}
<FormattedValueDisplay value={textValues} style={valueStyles} />
{showPercentChange && (
<PercentChange percentChange={percentChange} styles={layout.getPercentChangeStyles(percentChange)} />
<PercentChange
percentChange={percentChange}
styles={layout.getPercentChangeStyles(percentChange, percentChangeColorMode, valueStyles)}
/>
)}
</div>
{layout.renderChart()}

View File

@ -1,7 +1,17 @@
import { CSSProperties } from 'react';
import { createTheme, FieldType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { PercentChangeColorMode } from '@grafana/schema';
import { Props, BigValueColorMode, BigValueGraphMode, BigValueTextMode } from './BigValue';
import { buildLayout, StackedWithChartLayout, StackedWithNoChartLayout, WideWithChartLayout } from './BigValueLayout';
import {
buildLayout,
getPercentChangeColor,
StackedWithChartLayout,
StackedWithNoChartLayout,
WideWithChartLayout,
} from './BigValueLayout';
function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
@ -29,6 +39,10 @@ function getProps(propOverrides?: Partial<Props>): Props {
return props;
}
const valueStyles: CSSProperties = {
color: 'purple',
};
describe('BigValueLayout', () => {
describe('buildLayout', () => {
it('should auto select to stacked layout', () => {
@ -88,4 +102,70 @@ describe('BigValueLayout', () => {
expect(layout).toBeInstanceOf(WideWithChartLayout);
});
});
describe('percentChangeColor', () => {
const themeVisualizationColors = config.theme2.visualization;
const red = themeVisualizationColors.getColorByName('red');
const green = themeVisualizationColors.getColorByName('green');
it('standard negative should be red', () => {
const percentChange = -10;
const color = getPercentChangeColor(
percentChange,
PercentChangeColorMode.Standard,
valueStyles,
themeVisualizationColors
);
expect(color).toBe(red);
});
it('standard positive should be green', () => {
const percentChange = 10;
const color = getPercentChangeColor(
percentChange,
PercentChangeColorMode.Standard,
valueStyles,
themeVisualizationColors
);
expect(color).toBe(green);
});
it('inverted negative should be green', () => {
const percentChange = -10;
const color = getPercentChangeColor(
percentChange,
PercentChangeColorMode.Inverted,
valueStyles,
themeVisualizationColors
);
expect(color).toBe(green);
});
it('inverted positive should be red', () => {
const percentChange = 10;
const color = getPercentChangeColor(
percentChange,
PercentChangeColorMode.Inverted,
valueStyles,
themeVisualizationColors
);
expect(color).toBe(red);
});
it('same as value negative should be purple', () => {
const percentChange = -10;
const color = getPercentChangeColor(
percentChange,
PercentChangeColorMode.SameAsValue,
valueStyles,
themeVisualizationColors
);
expect(color).toBe('purple');
});
it('same as value positive should be purple', () => {
const percentChange = 10;
const color = getPercentChangeColor(
percentChange,
PercentChangeColorMode.SameAsValue,
valueStyles,
themeVisualizationColors
);
expect(color).toBe('purple');
});
});
});

View File

@ -1,8 +1,8 @@
import React, { CSSProperties } from 'react';
import tinycolor from 'tinycolor2';
import { formattedValueToString, DisplayValue, FieldConfig, FieldType } from '@grafana/data';
import { GraphDrawStyle, GraphFieldConfig } from '@grafana/schema';
import { formattedValueToString, DisplayValue, FieldConfig, FieldType, ThemeVisualizationColors } from '@grafana/data';
import { GraphDrawStyle, GraphFieldConfig, PercentChangeColorMode } from '@grafana/schema';
import { getTextColorForAlphaBackground } from '../../utils';
import { calculateFontSize } from '../../utils/measureText';
@ -103,16 +103,17 @@ export abstract class BigValueLayout {
return styles;
}
getPercentChangeStyles(percentChange: number): PercentChangeStyles {
getPercentChangeStyles(
percentChange: number,
percentChangeColorMode: PercentChangeColorMode | undefined,
valueStyles: React.CSSProperties
): PercentChangeStyles {
const VALUE_TO_PERCENT_CHANGE_RATIO = 2.5;
const valueContainerStyles = this.getValueAndTitleContainerStyles();
const percentFontSize = Math.max(this.valueFontSize / VALUE_TO_PERCENT_CHANGE_RATIO, 12);
let iconSize = Math.max(this.valueFontSize / 3, 10);
const color =
percentChange > 0
? this.props.theme.visualization.getColorByName('green')
: this.props.theme.visualization.getColorByName('red');
const themeVisualizationColors = this.props.theme.visualization;
const color = getPercentChangeColor(percentChange, percentChangeColorMode, valueStyles, themeVisualizationColors);
const containerStyles: CSSProperties = {
fontSize: percentFontSize,
@ -597,3 +598,18 @@ export interface PercentChangeStyles {
containerStyles: CSSProperties;
iconSize: number;
}
export function getPercentChangeColor(
percentChange: number,
percentChangeColorMode: PercentChangeColorMode | undefined,
valueStyles: CSSProperties,
themeVisualizationColors: ThemeVisualizationColors
): string | undefined {
if (percentChangeColorMode === PercentChangeColorMode.SameAsValue) {
return valueStyles.color;
} else {
return percentChange * (percentChangeColorMode === PercentChangeColorMode.Inverted ? -1 : 1) > 0
? themeVisualizationColors.getColorByName('green')
: themeVisualizationColors.getColorByName('red');
}
}

View File

@ -48,6 +48,7 @@ export class StatPanel extends PureComponent<PanelProps<Options>> {
onClick={openMenu}
className={targetClassName}
disableWideLayout={!options.wideLayout}
percentChangeColorMode={options.percentChangeColorMode}
/>
);
};

View File

@ -1,5 +1,11 @@
import { PanelPlugin } from '@grafana/data';
import { BigValueColorMode, BigValueGraphMode, BigValueJustifyMode, BigValueTextMode } from '@grafana/schema';
import {
BigValueColorMode,
BigValueGraphMode,
BigValueJustifyMode,
BigValueTextMode,
PercentChangeColorMode,
} from '@grafana/schema';
import { commonOptionsBuilder, sharedSingleStatMigrationHandler } from '@grafana/ui';
import { statPanelChangedHandler } from './StatMigrations';
@ -94,6 +100,20 @@ export const plugin = new PanelPlugin<Options>(StatPanel)
defaultValue: defaultOptions.showPercentChange,
category: mainCategory,
showIf: (config) => !config.reduceOptions.values,
})
.addSelect({
path: 'percentChangeColorMode',
name: 'Percent change color mode',
defaultValue: defaultOptions.percentChangeColorMode,
category: mainCategory,
settings: {
options: [
{ value: PercentChangeColorMode.Standard, label: 'Standard' },
{ value: PercentChangeColorMode.Inverted, label: 'Inverted' },
{ value: PercentChangeColorMode.SameAsValue, label: 'Same as Value' },
],
},
showIf: (config) => config.showPercentChange,
});
})
.setNoPadding()

View File

@ -27,12 +27,13 @@ composableKinds: PanelCfg: {
schema: {
Options: {
common.SingleStatBaseOptions
graphMode: common.BigValueGraphMode & (*"area" | _)
colorMode: common.BigValueColorMode & (*"value" | _)
justifyMode: common.BigValueJustifyMode & (*"auto" | _)
textMode: common.BigValueTextMode & (*"auto" | _)
wideLayout: bool | *true
showPercentChange: bool | *false
graphMode: common.BigValueGraphMode & (*"area" | _)
colorMode: common.BigValueColorMode & (*"value" | _)
justifyMode: common.BigValueJustifyMode & (*"auto" | _)
textMode: common.BigValueTextMode & (*"auto" | _)
wideLayout: bool | *true
showPercentChange: bool | *false
percentChangeColorMode: common.PercentChangeColorMode & (*"standard" | _)
} @cuetsy(kind="interface")
}
}]

View File

@ -14,6 +14,7 @@ export interface Options extends common.SingleStatBaseOptions {
colorMode: common.BigValueColorMode;
graphMode: common.BigValueGraphMode;
justifyMode: common.BigValueJustifyMode;
percentChangeColorMode: common.PercentChangeColorMode;
showPercentChange: boolean;
textMode: common.BigValueTextMode;
wideLayout: boolean;
@ -23,6 +24,7 @@ export const defaultOptions: Partial<Options> = {
colorMode: common.BigValueColorMode.Value,
graphMode: common.BigValueGraphMode.Area,
justifyMode: common.BigValueJustifyMode.Auto,
percentChangeColorMode: common.PercentChangeColorMode.Standard,
showPercentChange: false,
textMode: common.BigValueTextMode.Auto,
wideLayout: true,