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>
pull/79606/head
Drew Slobodnjak 1 year ago committed by GitHub
parent 2edcf0edbd
commit b166bdc3fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1417
      devenv/dev-dashboards/panel-stat/panel-stat-tests.json
  2. 21
      docs/sources/developers/kinds/composable/stat/panelcfg/schema-reference.md
  3. 8
      docs/sources/panels-visualizations/visualizations/stat/index.md
  4. 4
      packages/grafana-data/src/field/fieldDisplay.ts
  5. 4
      packages/grafana-data/src/types/displayValue.ts
  6. 2
      packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts
  7. 29
      packages/grafana-ui/src/components/BigValue/BigValue.test.tsx
  8. 6
      packages/grafana-ui/src/components/BigValue/BigValue.tsx
  9. 76
      packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx
  10. 28
      packages/grafana-ui/src/components/BigValue/PercentChange.tsx
  11. 1
      public/app/plugins/panel/stat/StatPanel.tsx
  12. 7
      public/app/plugins/panel/stat/module.tsx
  13. 11
      public/app/plugins/panel/stat/panelcfg.cue
  14. 2
      public/app/plugins/panel/stat/panelcfg.gen.ts

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…
Cancel
Save