mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Stat: Add Percent Change Option (#78250)
* Stat: Add Percent Change Option * Ensure div style only applied for percent change * Add metrics section to gdev * Apply new style and fix nan truthy * Handle no text case properly * Only display percent change with value * Improve styling * Remove VizOrientation dep and improve styling * Display percent change for text mode name * Add check for undefined percentChange * Don't show percent change option for all values * Make metric alignment more robust * Make percent change column case tighter Check undefined directly to avoid truthy issues * Simplify percentChange calculation * Add documentation for show percent change * Add tests for percent change * Refactor big value and pull out percent change * minor changes * initial approach at addressing setting % change colors to be conventional (not super happy with handling of contrast) * Clean up initial color change approach (no need to handle 0 case as is handled as NaN currently * Update shadow styling and include icon * Update docs/sources/panels-visualizations/visualizations/stat/index.md Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Stat: Add Percent Change Option (refactor and color exploration) (#79504) Co-authored-by: nmarrs <nathanielmarrs@gmail.com> * some missed cleanup :D * update percent change to show to not be tied to text value; update docs accordingly * initial start for fixing scaling of % change for no text mode * Fix styling for case where textmode is none * Tweak styling a bit for icon and minimum padding * Apply flex wrap to container styles * Update gdev for stat panel tests * attempt at fixing horizontal percent change styling / placement --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com> Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
2edcf0edbd
commit
b166bdc3fc
File diff suppressed because it is too large
Load Diff
@ -26,16 +26,17 @@ title: StatPanelCfg kind
|
||||
|
||||
It extends [SingleStatBaseOptions](#singlestatbaseoptions).
|
||||
|
||||
| Property | Type | Required | Default | Description |
|
||||
|-----------------|-------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `colorMode` | string | **Yes** | | TODO docs<br/>Possible values are: `value`, `background`, `background_solid`, `none`. |
|
||||
| `graphMode` | string | **Yes** | | TODO docs<br/>Possible values are: `none`, `line`, `area`. |
|
||||
| `justifyMode` | string | **Yes** | | TODO docs<br/>Possible values are: `auto`, `center`. |
|
||||
| `textMode` | string | **Yes** | | TODO docs<br/>Possible values are: `auto`, `value`, `value_and_name`, `name`, `none`. |
|
||||
| `wideLayout` | boolean | **Yes** | `true` | |
|
||||
| `orientation` | string | No | | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*<br/>TODO docs<br/>Possible values are: `auto`, `vertical`, `horizontal`. |
|
||||
| `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | No | | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*<br/>TODO docs |
|
||||
| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*<br/>TODO docs |
|
||||
| Property | Type | Required | Default | Description |
|
||||
|---------------------|-------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `colorMode` | string | **Yes** | | TODO docs<br/>Possible values are: `value`, `background`, `background_solid`, `none`. |
|
||||
| `graphMode` | string | **Yes** | | TODO docs<br/>Possible values are: `none`, `line`, `area`. |
|
||||
| `justifyMode` | string | **Yes** | | TODO docs<br/>Possible values are: `auto`, `center`. |
|
||||
| `showPercentChange` | boolean | **Yes** | `false` | |
|
||||
| `textMode` | string | **Yes** | | TODO docs<br/>Possible values are: `auto`, `value`, `value_and_name`, `name`, `none`. |
|
||||
| `wideLayout` | boolean | **Yes** | `true` | |
|
||||
| `orientation` | string | No | | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*<br/>TODO docs<br/>Possible values are: `auto`, `vertical`, `horizontal`. |
|
||||
| `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | No | | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*<br/>TODO docs |
|
||||
| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*<br/>TODO docs |
|
||||
|
||||
### ReduceDataOptions
|
||||
|
||||
|
@ -122,6 +122,14 @@ Choose an alignment mode.
|
||||
- **Auto -** If only a single value is shown (no repeat), then the value is centered. If multiple series or rows are shown, then the value is left-aligned.
|
||||
- **Center -** Stat value is centered.
|
||||
|
||||
### Show percent change
|
||||
|
||||
Set whether percent change is displayed or not. Disabled by default.
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
This option is not applicable when the **Show** setting, under **Value options**, is set to **All values**.
|
||||
{{% /admonition %}}
|
||||
|
||||
## Text size
|
||||
|
||||
Adjust the sizes of the gauge text.
|
||||
|
@ -72,6 +72,7 @@ export interface GetFieldDisplayValuesOptions {
|
||||
fieldConfig: FieldConfigSource;
|
||||
replaceVariables: InterpolateFunction;
|
||||
sparkline?: boolean; // Calculate the sparkline
|
||||
percentChange?: boolean; // Calculate percent change
|
||||
theme: GrafanaTheme2;
|
||||
timeZone?: TimeZone;
|
||||
}
|
||||
@ -186,6 +187,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
} else {
|
||||
displayValue.title = getFieldDisplayName(field, dataFrame, data);
|
||||
}
|
||||
displayValue.percentChange = options.percentChange
|
||||
? reduceField({ field: field, reducers: [ReducerID.diffperc] }).diffperc
|
||||
: undefined;
|
||||
|
||||
let sparkline: FieldSparkline | undefined = undefined;
|
||||
if (options.sparkline) {
|
||||
|
@ -11,6 +11,10 @@ export interface DisplayValue extends FormattedValue {
|
||||
* 0-1 between min & max
|
||||
*/
|
||||
percent?: number;
|
||||
/**
|
||||
* 0-1 percent change across range
|
||||
*/
|
||||
percentChange?: number;
|
||||
/**
|
||||
* Color based on mappings or threshold
|
||||
*/
|
||||
|
@ -17,6 +17,7 @@ export interface Options extends common.SingleStatBaseOptions {
|
||||
colorMode: common.BigValueColorMode;
|
||||
graphMode: common.BigValueGraphMode;
|
||||
justifyMode: common.BigValueJustifyMode;
|
||||
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,
|
||||
showPercentChange: false,
|
||||
textMode: common.BigValueTextMode.Auto,
|
||||
wideLayout: true,
|
||||
};
|
||||
|
@ -5,17 +5,19 @@ import { createTheme } from '@grafana/data';
|
||||
|
||||
import { BigValue, BigValueColorMode, BigValueGraphMode, Props } from './BigValue';
|
||||
|
||||
const valueObject = {
|
||||
text: '25',
|
||||
numeric: 25,
|
||||
color: 'red',
|
||||
};
|
||||
|
||||
function getProps(propOverrides?: Partial<Props>): Props {
|
||||
const props: Props = {
|
||||
colorMode: BigValueColorMode.Background,
|
||||
graphMode: BigValueGraphMode.Line,
|
||||
height: 300,
|
||||
width: 300,
|
||||
value: {
|
||||
text: '25',
|
||||
numeric: 25,
|
||||
color: 'red',
|
||||
},
|
||||
value: valueObject,
|
||||
theme: createTheme(),
|
||||
};
|
||||
|
||||
@ -30,5 +32,22 @@ describe('BigValue', () => {
|
||||
|
||||
expect(screen.getByText('25')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with percent change', () => {
|
||||
render(
|
||||
<BigValue
|
||||
{...getProps({
|
||||
value: { ...valueObject, percentChange: 0.5 },
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('50%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render without percent change', () => {
|
||||
render(<BigValue {...getProps()} />);
|
||||
expect(screen.queryByText('%')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import { clearButtonStyles } from '../Button';
|
||||
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
|
||||
|
||||
import { buildLayout } from './BigValueLayout';
|
||||
import { PercentChange } from './PercentChange';
|
||||
|
||||
export enum BigValueColorMode {
|
||||
Background = 'background',
|
||||
@ -92,6 +93,8 @@ export class BigValue extends PureComponent<Props> {
|
||||
const valueStyles = layout.getValueStyles();
|
||||
const titleStyles = layout.getTitleStyles();
|
||||
const textValues = layout.textValues;
|
||||
const percentChange = this.props.value.percentChange;
|
||||
const showPercentChange = percentChange != null && !Number.isNaN(percentChange);
|
||||
|
||||
// When there is an outer data link this tooltip will override the outer native tooltip
|
||||
const tooltip = hasLinks ? undefined : textValues.tooltip;
|
||||
@ -102,6 +105,9 @@ export class BigValue extends PureComponent<Props> {
|
||||
<div style={valueAndTitleContainerStyles}>
|
||||
{textValues.title && <div style={titleStyles}>{textValues.title}</div>}
|
||||
<FormattedValueDisplay value={textValues} style={valueStyles} />
|
||||
{showPercentChange && (
|
||||
<PercentChange percentChange={percentChange} styles={layout.getPercentChangeStyles(percentChange)} />
|
||||
)}
|
||||
</div>
|
||||
{layout.renderChart()}
|
||||
</div>
|
||||
|
@ -9,6 +9,7 @@ import { calculateFontSize } from '../../utils/measureText';
|
||||
import { Sparkline } from '../Sparkline/Sparkline';
|
||||
|
||||
import { BigValueColorMode, Props, BigValueJustifyMode, BigValueTextMode } from './BigValue';
|
||||
import { percentChangeString } from './PercentChange';
|
||||
|
||||
const LINE_HEIGHT = 1.2;
|
||||
const MAX_TITLE_SIZE = 30;
|
||||
@ -102,9 +103,75 @@ export abstract class BigValueLayout {
|
||||
return styles;
|
||||
}
|
||||
|
||||
getPercentChangeStyles(percentChange: number): 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 containerStyles: CSSProperties = {
|
||||
fontSize: percentFontSize,
|
||||
fontWeight: VALUE_FONT_WEIGHT,
|
||||
lineHeight: LINE_HEIGHT,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: Math.max(percentFontSize / 3, 4),
|
||||
zIndex: 1,
|
||||
color,
|
||||
};
|
||||
|
||||
if (this.justifyCenter) {
|
||||
containerStyles.textAlign = 'center';
|
||||
}
|
||||
|
||||
if (valueContainerStyles.flexDirection === 'column' && percentFontSize > 12) {
|
||||
containerStyles.marginTop = -(percentFontSize / 4);
|
||||
}
|
||||
|
||||
if (valueContainerStyles.flexDirection === 'row') {
|
||||
containerStyles.alignItems = 'baseline';
|
||||
|
||||
// Center the percent change vertically relative to the value
|
||||
// This approach seems to work the best for all edge cases
|
||||
// Note: the fixed min font size causes this to be off for a few edge cases
|
||||
containerStyles.lineHeight = LINE_HEIGHT * VALUE_TO_PERCENT_CHANGE_RATIO;
|
||||
}
|
||||
|
||||
switch (this.props.colorMode) {
|
||||
case BigValueColorMode.Background:
|
||||
case BigValueColorMode.BackgroundSolid:
|
||||
containerStyles.color = getTextColorForAlphaBackground(this.valueColor, this.props.theme.isDark);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.props.textMode === BigValueTextMode.None) {
|
||||
containerStyles.fontSize = calculateFontSize(
|
||||
percentChangeString(percentChange),
|
||||
this.maxTextWidth * 0.8,
|
||||
this.maxTextHeight * 0.8,
|
||||
LINE_HEIGHT,
|
||||
undefined,
|
||||
VALUE_FONT_WEIGHT
|
||||
);
|
||||
iconSize = containerStyles.fontSize * 0.8;
|
||||
}
|
||||
|
||||
return {
|
||||
containerStyles,
|
||||
iconSize: iconSize,
|
||||
};
|
||||
}
|
||||
|
||||
getValueAndTitleContainerStyles() {
|
||||
const styles: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
};
|
||||
|
||||
if (this.justifyCenter) {
|
||||
@ -118,12 +185,12 @@ export abstract class BigValueLayout {
|
||||
}
|
||||
|
||||
getPanelStyles(): CSSProperties {
|
||||
const { width, height, theme, colorMode } = this.props;
|
||||
const { width, height, theme, colorMode, textMode } = this.props;
|
||||
|
||||
const panelStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
padding: `${this.panelPadding}px`,
|
||||
padding: `${textMode === BigValueTextMode.None ? 2 : this.panelPadding}px`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
@ -525,3 +592,8 @@ function getTextValues(props: Props): BigValueTextValues {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface PercentChangeStyles {
|
||||
containerStyles: CSSProperties;
|
||||
iconSize: number;
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { PercentChangeStyles } from './BigValueLayout';
|
||||
|
||||
export interface Props {
|
||||
percentChange: number;
|
||||
styles: PercentChangeStyles;
|
||||
}
|
||||
|
||||
export const PercentChange = ({ percentChange, styles }: Props) => {
|
||||
const percentChangeIcon =
|
||||
percentChange && (percentChange > 0 ? 'arrow-up' : percentChange < 0 ? 'arrow-down' : undefined);
|
||||
|
||||
return (
|
||||
<div style={styles.containerStyles}>
|
||||
{percentChangeIcon && (
|
||||
<Icon name={percentChangeIcon} height={styles.iconSize} width={styles.iconSize} viewBox="6 6 12 12" />
|
||||
)}
|
||||
{percentChangeString(percentChange)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const percentChangeString = (percentChange: number) => {
|
||||
return percentChange?.toLocaleString(undefined, { style: 'percent', maximumSignificantDigits: 3 }) ?? '';
|
||||
};
|
@ -112,6 +112,7 @@ export class StatPanel extends PureComponent<PanelProps<Options>> {
|
||||
theme: config.theme2,
|
||||
data: data.series,
|
||||
sparkline: options.graphMode !== BigValueGraphMode.None,
|
||||
percentChange: options.showPercentChange,
|
||||
timeZone,
|
||||
});
|
||||
};
|
||||
|
@ -87,6 +87,13 @@ export const plugin = new PanelPlugin<Options>(StatPanel)
|
||||
{ value: BigValueJustifyMode.Center, label: 'Center' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showPercentChange',
|
||||
name: 'Show percent change',
|
||||
defaultValue: defaultOptions.showPercentChange,
|
||||
category: mainCategory,
|
||||
showIf: (config) => !config.reduceOptions.values,
|
||||
});
|
||||
})
|
||||
.setNoPadding()
|
||||
|
@ -27,11 +27,12 @@ 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
|
||||
graphMode: common.BigValueGraphMode & (*"area" | _)
|
||||
colorMode: common.BigValueColorMode & (*"value" | _)
|
||||
justifyMode: common.BigValueJustifyMode & (*"auto" | _)
|
||||
textMode: common.BigValueTextMode & (*"auto" | _)
|
||||
wideLayout: bool | *true
|
||||
showPercentChange: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
|
@ -14,6 +14,7 @@ export interface Options extends common.SingleStatBaseOptions {
|
||||
colorMode: common.BigValueColorMode;
|
||||
graphMode: common.BigValueGraphMode;
|
||||
justifyMode: common.BigValueJustifyMode;
|
||||
showPercentChange: boolean;
|
||||
textMode: common.BigValueTextMode;
|
||||
wideLayout: boolean;
|
||||
}
|
||||
@ -22,6 +23,7 @@ export const defaultOptions: Partial<Options> = {
|
||||
colorMode: common.BigValueColorMode.Value,
|
||||
graphMode: common.BigValueGraphMode.Area,
|
||||
justifyMode: common.BigValueJustifyMode.Auto,
|
||||
showPercentChange: false,
|
||||
textMode: common.BigValueTextMode.Auto,
|
||||
wideLayout: true,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user