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:
Drew Slobodnjak
2023-12-15 16:15:31 -08:00
committed by GitHub
parent 2edcf0edbd
commit b166bdc3fc
14 changed files with 1485 additions and 101 deletions

View File

@@ -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();
});
});
});

View File

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

View File

@@ -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;
}

View File

@@ -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 }) ?? '';
};