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:
@@ -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 }) ?? '';
|
||||
};
|
||||
Reference in New Issue
Block a user