Stat/Gauge: expose explicit font sizing (#29476)

This commit is contained in:
Ryan McKinley 2020-12-04 10:03:59 -08:00 committed by GitHub
parent 7236a44a4f
commit 716117b7da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 141 additions and 54 deletions

View File

@ -18,6 +18,17 @@ export interface DisplayValue extends FormattedValue {
title?: string; title?: string;
} }
/**
* Explicit control for text settings
*/
export interface TextDisplayOptions {
/* Explicit text size */
titleSize?: number;
/* Explicit text size */
valueSize?: number;
}
/** /**
* These represents the display value with the longest title and text. * These represents the display value with the longest title and text.
* Used to align widths and heights when displaying multiple DisplayValues * Used to align widths and heights when displaying multiple DisplayValues

View File

@ -14,6 +14,7 @@ import {
getFieldColorMode, getFieldColorMode,
getColorForTheme, getColorForTheme,
FALLBACK_COLOR, FALLBACK_COLOR,
TextDisplayOptions,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -42,6 +43,7 @@ export interface Props extends Themeable {
display?: DisplayProcessor; display?: DisplayProcessor;
value: DisplayValue; value: DisplayValue;
orientation: VizOrientation; orientation: VizOrientation;
text?: TextDisplayOptions;
itemSpacing?: number; itemSpacing?: number;
lcdCellWidth?: number; lcdCellWidth?: number;
displayMode: BarGaugeDisplayMode; displayMode: BarGaugeDisplayMode;
@ -172,7 +174,7 @@ export class BarGauge extends PureComponent<Props> {
} }
renderRetroBars(): ReactNode { renderRetroBars(): ReactNode {
const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props; const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props;
const { const {
valueHeight, valueHeight,
valueWidth, valueWidth,
@ -193,7 +195,7 @@ export class BarGauge extends PureComponent<Props> {
const valueColor = getValueColor(this.props); const valueColor = getValueColor(this.props);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation); const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text);
const containerStyles: CSSProperties = { const containerStyles: CSSProperties = {
width: `${wrapperWidth}px`, width: `${wrapperWidth}px`,
@ -270,7 +272,7 @@ function isVertical(orientation: VizOrientation) {
} }
function calculateTitleDimensions(props: Props): TitleDimensions { function calculateTitleDimensions(props: Props): TitleDimensions {
const { height, width, alignmentFactors, orientation } = props; const { height, width, alignmentFactors, orientation, text } = props;
const title = alignmentFactors ? alignmentFactors.title : props.value.title; const title = alignmentFactors ? alignmentFactors.title : props.value.title;
if (!title) { if (!title) {
@ -278,16 +280,26 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
} }
if (isVertical(orientation)) { if (isVertical(orientation)) {
const fontSize = text?.titleSize ?? 14;
return { return {
fontSize: 14, fontSize: fontSize,
width: width, width: width,
height: 14 * TITLE_LINE_HEIGHT, height: fontSize * TITLE_LINE_HEIGHT,
placement: 'below', placement: 'below',
}; };
} }
// if height above 40 put text to above bar // if height above 40 put text to above bar
if (height > 40) { if (height > 40) {
if (text?.titleSize) {
return {
fontSize: text?.titleSize,
width: 0,
height: text.titleSize * TITLE_LINE_HEIGHT,
placement: 'above',
};
}
const maxTitleHeightRatio = 0.45; const maxTitleHeightRatio = 0.45;
const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17); const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17);
@ -306,7 +318,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
const textSize = measureText(title, titleFontSize); const textSize = measureText(title, titleFontSize);
return { return {
fontSize: titleFontSize, fontSize: text?.titleSize ?? titleFontSize,
height: 0, height: 0,
width: textSize.width + 15, width: textSize.width + 15,
placement: 'left', placement: 'left',
@ -370,7 +382,7 @@ interface BarAndValueDimensions {
} }
function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions { function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
const { height, width, orientation } = props; const { height, width, orientation, text } = props;
const titleDim = calculateTitleDimensions(props); const titleDim = calculateTitleDimensions(props);
let maxBarHeight = 0; let maxBarHeight = 0;
@ -381,14 +393,23 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
let wrapperHeight = 0; let wrapperHeight = 0;
if (isVertical(orientation)) { if (isVertical(orientation)) {
if (text?.valueSize) {
valueHeight = text.valueSize * VALUE_LINE_HEIGHT;
} else {
valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT); valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT);
}
valueWidth = width; valueWidth = width;
maxBarHeight = height - (titleDim.height + valueHeight); maxBarHeight = height - (titleDim.height + valueHeight);
maxBarWidth = width; maxBarWidth = width;
wrapperWidth = width; wrapperWidth = width;
wrapperHeight = height - titleDim.height; wrapperHeight = height - titleDim.height;
} else {
if (text?.valueSize) {
valueHeight = text.valueSize * VALUE_LINE_HEIGHT;
} else { } else {
valueHeight = height - titleDim.height; valueHeight = height - titleDim.height;
}
valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), MIN_VALUE_WIDTH); valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), MIN_VALUE_WIDTH);
maxBarHeight = height - titleDim.height; maxBarHeight = height - titleDim.height;
maxBarWidth = width - valueWidth - titleDim.width; maxBarWidth = width - valueWidth - titleDim.width;
@ -420,14 +441,14 @@ export function getValuePercent(value: number, minValue: number, maxValue: numbe
* Only exported to for unit test * Only exported to for unit test
*/ */
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles { export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
const { displayMode, field, value, alignmentFactors, orientation, theme } = props; const { displayMode, field, value, alignmentFactors, orientation, theme, text } = props;
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props); const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
const valuePercent = getValuePercent(value.numeric, field.min!, field.max!); const valuePercent = getValuePercent(value.numeric, field.min!, field.max!);
const valueColor = getValueColor(props); const valueColor = getValueColor(props);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation); const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text);
const isBasic = displayMode === 'basic'; const isBasic = displayMode === 'basic';
const wrapperStyles: CSSProperties = { const wrapperStyles: CSSProperties = {
@ -581,7 +602,8 @@ function getValueStyles(
color: string, color: string,
width: number, width: number,
height: number, height: number,
orientation: VizOrientation orientation: VizOrientation,
text?: TextDisplayOptions
): CSSProperties { ): CSSProperties {
const styles: CSSProperties = { const styles: CSSProperties = {
color, color,
@ -597,15 +619,12 @@ function getValueStyles(
const formattedValueString = formattedValueToString(value); const formattedValueString = formattedValueToString(value);
if (isVertical(orientation)) { if (isVertical(orientation)) {
styles.fontSize = calculateFontSize(formattedValueString, textWidth, height, VALUE_LINE_HEIGHT); styles.fontSize = text?.valueSize ?? calculateFontSize(formattedValueString, textWidth, height, VALUE_LINE_HEIGHT);
styles.justifyContent = `center`; styles.justifyContent = `center`;
} else { } else {
styles.fontSize = calculateFontSize( styles.fontSize =
formattedValueString, text?.valueSize ??
textWidth - VALUE_LEFT_PADDING * 2, calculateFontSize(formattedValueString, textWidth - VALUE_LEFT_PADDING * 2, height, VALUE_LINE_HEIGHT);
height,
VALUE_LINE_HEIGHT
);
styles.justifyContent = `flex-end`; styles.justifyContent = `flex-end`;
styles.paddingLeft = `${VALUE_LEFT_PADDING}px`; styles.paddingLeft = `${VALUE_LEFT_PADDING}px`;
styles.paddingRight = `${VALUE_LEFT_PADDING}px`; styles.paddingRight = `${VALUE_LEFT_PADDING}px`;

View File

@ -1,6 +1,6 @@
// Library // Library
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { DisplayValue, GraphSeriesValue, DisplayValueAlignmentFactors } from '@grafana/data'; import { DisplayValue, GraphSeriesValue, DisplayValueAlignmentFactors, TextDisplayOptions } from '@grafana/data';
// Types // Types
import { Themeable } from '../../types'; import { Themeable } from '../../types';
@ -64,6 +64,8 @@ export interface Props extends Themeable {
justifyMode?: BigValueJustifyMode; justifyMode?: BigValueJustifyMode;
/** Factors that should influence the positioning of the text */ /** Factors that should influence the positioning of the text */
alignmentFactors?: DisplayValueAlignmentFactors; alignmentFactors?: DisplayValueAlignmentFactors;
/** Explicit font size control */
text?: TextDisplayOptions;
/** Specify which text should be visible in the BigValue */ /** Specify which text should be visible in the BigValue */
textMode?: BigValueTextMode; textMode?: BigValueTextMode;

View File

@ -29,7 +29,7 @@ export abstract class BigValueLayout {
textValues: BigValueTextValues; textValues: BigValueTextValues;
constructor(private props: Props) { constructor(private props: Props) {
const { width, height, value, theme } = props; const { width, height, value, theme, text } = props;
this.valueColor = getColorForTheme(value.color || 'green', theme); this.valueColor = getColorForTheme(value.color || 'green', theme);
this.panelPadding = height > 100 ? 12 : 8; this.panelPadding = height > 100 ? 12 : 8;
@ -43,6 +43,18 @@ export abstract class BigValueLayout {
this.chartWidth = 0; this.chartWidth = 0;
this.maxTextWidth = width - this.panelPadding * 2; this.maxTextWidth = width - this.panelPadding * 2;
this.maxTextHeight = height - this.panelPadding * 2; this.maxTextHeight = height - this.panelPadding * 2;
// Explicit font sizing
if (text) {
if (text.titleSize) {
this.titleFontSize = text.titleSize;
this.titleToAlignTo = undefined;
}
if (text.valueSize) {
this.valueFontSize = text.valueSize;
this.valueToAlignTo = '';
}
}
} }
getTitleStyles(): CSSProperties { getTitleStyles(): CSSProperties {
@ -235,9 +247,9 @@ export class WideNoChartLayout extends BigValueLayout {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const valueWidthPercent = 0.3; const valueWidthPercent = this.titleToAlignTo?.length ? 0.3 : 1.0;
if (this.titleToAlignTo && this.titleToAlignTo.length > 0) { if (this.valueToAlignTo.length) {
// initial value size // initial value size
this.valueFontSize = calculateFontSize( this.valueFontSize = calculateFontSize(
this.valueToAlignTo, this.valueToAlignTo,
@ -245,7 +257,9 @@ export class WideNoChartLayout extends BigValueLayout {
this.maxTextHeight, this.maxTextHeight,
LINE_HEIGHT LINE_HEIGHT
); );
}
if (this.titleToAlignTo?.length) {
// How big can we make the title and still have it fit // How big can we make the title and still have it fit
this.titleFontSize = calculateFontSize( this.titleFontSize = calculateFontSize(
this.titleToAlignTo, this.titleToAlignTo,
@ -257,9 +271,6 @@ export class WideNoChartLayout extends BigValueLayout {
// make sure it's a bit smaller than valueFontSize // make sure it's a bit smaller than valueFontSize
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize); this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
} else {
// if no title wide
this.valueFontSize = calculateFontSize(this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight, LINE_HEIGHT);
} }
} }
@ -292,6 +303,7 @@ export class WideWithChartLayout extends BigValueLayout {
super(props); super(props);
const { width, height } = props; const { width, height } = props;
const chartHeightPercent = 0.5; const chartHeightPercent = 0.5;
const titleWidthPercent = 0.6; const titleWidthPercent = 0.6;
const valueWidthPercent = 1 - titleWidthPercent; const valueWidthPercent = 1 - titleWidthPercent;
@ -300,7 +312,7 @@ export class WideWithChartLayout extends BigValueLayout {
this.chartWidth = width; this.chartWidth = width;
this.chartHeight = height * chartHeightPercent; this.chartHeight = height * chartHeightPercent;
if (this.titleToAlignTo && this.titleToAlignTo.length > 0) { if (this.titleToAlignTo?.length) {
this.titleFontSize = calculateFontSize( this.titleFontSize = calculateFontSize(
this.titleToAlignTo, this.titleToAlignTo,
this.maxTextWidth * titleWidthPercent, this.maxTextWidth * titleWidthPercent,
@ -310,6 +322,7 @@ export class WideWithChartLayout extends BigValueLayout {
); );
} }
if (this.valueToAlignTo.length) {
this.valueFontSize = calculateFontSize( this.valueFontSize = calculateFontSize(
this.valueToAlignTo, this.valueToAlignTo,
this.maxTextWidth * valueWidthPercent, this.maxTextWidth * valueWidthPercent,
@ -317,6 +330,7 @@ export class WideWithChartLayout extends BigValueLayout {
LINE_HEIGHT LINE_HEIGHT
); );
} }
}
getValueAndTitleContainerStyles() { getValueAndTitleContainerStyles() {
const styles = super.getValueAndTitleContainerStyles(); const styles = super.getValueAndTitleContainerStyles();
@ -350,7 +364,7 @@ export class StackedWithChartLayout extends BigValueLayout {
this.chartHeight = height * chartHeightPercent; this.chartHeight = height * chartHeightPercent;
this.chartWidth = width; this.chartWidth = width;
if (this.titleToAlignTo && this.titleToAlignTo.length > 0) { if (this.titleToAlignTo?.length) {
this.titleFontSize = calculateFontSize( this.titleFontSize = calculateFontSize(
this.titleToAlignTo, this.titleToAlignTo,
this.maxTextWidth, this.maxTextWidth,
@ -358,19 +372,22 @@ export class StackedWithChartLayout extends BigValueLayout {
LINE_HEIGHT, LINE_HEIGHT,
MAX_TITLE_SIZE MAX_TITLE_SIZE
); );
titleHeight = this.titleFontSize * LINE_HEIGHT;
} }
titleHeight = this.titleFontSize * LINE_HEIGHT;
if (this.valueToAlignTo.length) {
this.valueFontSize = calculateFontSize( this.valueFontSize = calculateFontSize(
this.valueToAlignTo, this.valueToAlignTo,
this.maxTextWidth, this.maxTextWidth,
this.maxTextHeight - this.chartHeight - titleHeight, this.maxTextHeight - this.chartHeight - titleHeight,
LINE_HEIGHT LINE_HEIGHT
); );
}
// make title fontsize it's a bit smaller than valueFontSize // make title fontsize it's a bit smaller than valueFontSize
if (this.titleToAlignTo?.length) {
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize); this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
}
// make chart take up unused space // make chart take up unused space
this.chartHeight = height - this.titleFontSize * LINE_HEIGHT - this.valueFontSize * LINE_HEIGHT; this.chartHeight = height - this.titleFontSize * LINE_HEIGHT - this.valueFontSize * LINE_HEIGHT;
@ -398,7 +415,7 @@ export class StackedWithNoChartLayout extends BigValueLayout {
const titleHeightPercent = 0.15; const titleHeightPercent = 0.15;
let titleHeight = 0; let titleHeight = 0;
if (this.titleToAlignTo && this.titleToAlignTo.length > 0) { if (this.titleToAlignTo?.length) {
this.titleFontSize = calculateFontSize( this.titleFontSize = calculateFontSize(
this.titleToAlignTo, this.titleToAlignTo,
this.maxTextWidth, this.maxTextWidth,
@ -410,12 +427,14 @@ export class StackedWithNoChartLayout extends BigValueLayout {
titleHeight = this.titleFontSize * LINE_HEIGHT; titleHeight = this.titleFontSize * LINE_HEIGHT;
} }
if (this.valueToAlignTo.length) {
this.valueFontSize = calculateFontSize( this.valueFontSize = calculateFontSize(
this.valueToAlignTo, this.valueToAlignTo,
this.maxTextWidth, this.maxTextWidth,
this.maxTextHeight - titleHeight, this.maxTextHeight - titleHeight,
LINE_HEIGHT LINE_HEIGHT
); );
}
// make title fontsize it's a bit smaller than valueFontSize // make title fontsize it's a bit smaller than valueFontSize
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize); this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);

View File

@ -10,6 +10,7 @@ import {
getColorForTheme, getColorForTheme,
FieldColorModeId, FieldColorModeId,
FALLBACK_COLOR, FALLBACK_COLOR,
TextDisplayOptions,
} from '@grafana/data'; } from '@grafana/data';
import { Themeable } from '../../types'; import { Themeable } from '../../types';
import { calculateFontSize } from '../../utils/measureText'; import { calculateFontSize } from '../../utils/measureText';
@ -21,6 +22,7 @@ export interface Props extends Themeable {
showThresholdLabels: boolean; showThresholdLabels: boolean;
width: number; width: number;
value: DisplayValue; value: DisplayValue;
text?: TextDisplayOptions;
onClick?: React.MouseEventHandler<HTMLElement>; onClick?: React.MouseEventHandler<HTMLElement>;
className?: string; className?: string;
} }
@ -108,7 +110,7 @@ export class Gauge extends PureComponent<Props> {
// remove gauge & marker width (on left and right side) // remove gauge & marker width (on left and right side)
// and 10px is some padding that flot adds to the outer canvas // and 10px is some padding that flot adds to the outer canvas
const valueWidth = valueWidthBase - ((gaugeWidth + (showThresholdMarkers ? thresholdMarkersWidth : 0)) * 2 + 10); const valueWidth = valueWidthBase - ((gaugeWidth + (showThresholdMarkers ? thresholdMarkersWidth : 0)) * 2 + 10);
const fontSize = calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7); const fontSize = this.props.text?.valueSize ?? calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7);
const thresholdLabelFontSize = fontSize / 2.5; const thresholdLabelFontSize = fontSize / 2.5;
let min = field.min!; let min = field.min!;
@ -180,7 +182,7 @@ export class Gauge extends PureComponent<Props> {
} }
renderVisualization = () => { renderVisualization = () => {
const { width, value, height, onClick } = this.props; const { width, value, height, onClick, text } = this.props;
const autoProps = calculateGaugeAutoProps(width, height, value.title); const autoProps = calculateGaugeAutoProps(width, height, value.title);
return ( return (
@ -194,7 +196,7 @@ export class Gauge extends PureComponent<Props> {
<div <div
style={{ style={{
textAlign: 'center', textAlign: 'center',
fontSize: autoProps.titleFontSize, fontSize: text?.titleSize ?? autoProps.titleFontSize,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',

View File

@ -16,11 +16,13 @@ import {
ThresholdsConfig, ThresholdsConfig,
validateFieldConfig, validateFieldConfig,
FieldColorModeId, FieldColorModeId,
TextDisplayOptions,
} from '@grafana/data'; } from '@grafana/data';
export interface SingleStatBaseOptions { export interface SingleStatBaseOptions {
reduceOptions: ReduceDataOptions; reduceOptions: ReduceDataOptions;
orientation: VizOrientation; orientation: VizOrientation;
text?: TextDisplayOptions;
} }
const optionsToKeep = ['reduceOptions', 'orientation']; const optionsToKeep = ['reduceOptions', 'orientation'];

View File

@ -38,6 +38,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
height={height} height={height}
orientation={orientation} orientation={orientation}
field={field} field={field}
text={options.text}
display={processor} display={processor}
theme={config.theme} theme={config.theme}
itemSpacing={this.getItemSpacing()} itemSpacing={this.getItemSpacing()}

View File

@ -23,6 +23,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
width={width} width={width}
height={height} height={height}
field={field} field={field}
text={options.text}
showThresholdLabels={options.showThresholdLabels} showThresholdLabels={options.showThresholdLabels}
showThresholdMarkers={options.showThresholdMarkers} showThresholdMarkers={options.showThresholdMarkers}
theme={config.theme} theme={config.theme}

View File

@ -56,6 +56,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
justifyMode={options.justifyMode} justifyMode={options.justifyMode}
textMode={this.getTextMode()} textMode={this.getTextMode()}
alignmentFactors={alignmentFactors} alignmentFactors={alignmentFactors}
text={options.text}
width={width} width={width}
height={height} height={height}
theme={config.theme} theme={config.theme}

View File

@ -25,7 +25,8 @@ export interface StatPanelOptions extends SingleStatBaseOptions {
export function addStandardDataReduceOptions( export function addStandardDataReduceOptions(
builder: PanelOptionsEditorBuilder<SingleStatBaseOptions>, builder: PanelOptionsEditorBuilder<SingleStatBaseOptions>,
includeOrientation = true, includeOrientation = true,
includeFieldMatcher = true includeFieldMatcher = true,
includeTextSizes = true
) { ) {
builder.addRadio({ builder.addRadio({
path: 'reduceOptions.values', path: 'reduceOptions.values',
@ -108,4 +109,32 @@ export function addStandardDataReduceOptions(
defaultValue: 'auto', defaultValue: 'auto',
}); });
} }
if (includeTextSizes) {
builder.addNumberInput({
path: 'text.titleSize',
category: ['Text size'],
name: 'Title',
settings: {
placeholder: 'Auto',
integer: false,
min: 1,
max: 200,
},
defaultValue: undefined,
});
builder.addNumberInput({
path: 'text.valueSize',
category: ['Text size'],
name: 'Value',
settings: {
placeholder: 'Auto',
integer: false,
min: 1,
max: 200,
},
defaultValue: undefined,
});
}
} }