// Libraries import React, { CSSProperties } from 'react'; import tinycolor from 'tinycolor2'; // Utils import { formattedValueToString, DisplayValue, getColorForTheme, FieldConfig } from '@grafana/data'; import { calculateFontSize } from '../../utils/measureText'; // Types import { BigValueColorMode, Props, BigValueJustifyMode, BigValueTextMode } from './BigValue'; import { getTextColorForBackground } from '../../utils'; import { DrawStyle, GraphFieldConfig } from '../uPlot/config'; import { Sparkline } from '../Sparkline/Sparkline'; const LINE_HEIGHT = 1.2; const MAX_TITLE_SIZE = 30; export abstract class BigValueLayout { titleFontSize: number; valueFontSize: number; chartHeight: number; chartWidth: number; valueColor: string; panelPadding: number; justifyCenter: boolean; titleToAlignTo?: string; valueToAlignTo: string; maxTextWidth: number; maxTextHeight: number; textValues: BigValueTextValues; constructor(private props: Props) { const { width, height, value, theme, text } = props; this.valueColor = getColorForTheme(value.color || 'green', theme); this.panelPadding = height > 100 ? 12 : 8; this.textValues = getTextValues(props); this.justifyCenter = shouldJustifyCenter(props.justifyMode, this.textValues.title); this.valueToAlignTo = this.textValues.valueToAlignTo; this.titleToAlignTo = this.textValues.titleToAlignTo; this.titleFontSize = 14; this.valueFontSize = 14; this.chartHeight = 0; this.chartWidth = 0; this.maxTextWidth = width - 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 { const styles: CSSProperties = { fontSize: `${this.titleFontSize}px`, lineHeight: LINE_HEIGHT, }; if (this.props.colorMode === BigValueColorMode.Background) { styles.color = getTextColorForBackground(this.valueColor); } return styles; } getValueStyles(): CSSProperties { const styles: CSSProperties = { fontSize: this.valueFontSize, fontWeight: 500, lineHeight: LINE_HEIGHT, position: 'relative', zIndex: 1, }; if (this.justifyCenter) { styles.textAlign = 'center'; } switch (this.props.colorMode) { case BigValueColorMode.Value: styles.color = this.valueColor; break; case BigValueColorMode.Background: styles.color = getTextColorForBackground(this.valueColor); } return styles; } getValueAndTitleContainerStyles() { const styles: CSSProperties = { display: 'flex', }; if (this.justifyCenter) { styles.alignItems = 'center'; styles.justifyContent = 'center'; styles.flexGrow = 1; } return styles; } getPanelStyles(): CSSProperties { const { width, height, theme, colorMode } = this.props; const panelStyles: CSSProperties = { width: `${width}px`, height: `${height}px`, padding: `${this.panelPadding}px`, borderRadius: '3px', position: 'relative', display: 'flex', }; const themeFactor = theme.isDark ? 1 : -0.7; switch (colorMode) { case BigValueColorMode.Background: const bgColor2 = tinycolor(this.valueColor) .darken(15 * themeFactor) .spin(8) .toRgbString(); const bgColor3 = tinycolor(this.valueColor) .darken(5 * themeFactor) .spin(-8) .toRgbString(); panelStyles.background = `linear-gradient(120deg, ${bgColor2}, ${bgColor3})`; break; case BigValueColorMode.Value: panelStyles.background = `transparent`; break; } if (this.justifyCenter) { panelStyles.alignItems = 'center'; panelStyles.flexDirection = 'row'; } return panelStyles; } renderChart(): JSX.Element | null { const { sparkline, colorMode } = this.props; if (!sparkline || !sparkline.y) { return null; } let fillColor: string; let lineColor: string; switch (colorMode) { case BigValueColorMode.Value: lineColor = this.valueColor; fillColor = tinycolor(this.valueColor).setAlpha(0.2).toRgbString(); break; case BigValueColorMode.Background: fillColor = 'rgba(255,255,255,0.4)'; lineColor = tinycolor(this.valueColor).brighten(40).toRgbString(); } // The graph field configuration applied to Y values const config: FieldConfig = { custom: { drawStyle: DrawStyle.Line, lineWidth: 1, fillColor, lineColor, }, }; return (
); } getChartStyles(): CSSProperties { return { position: 'absolute', right: 0, bottom: 0, }; } } export class WideNoChartLayout extends BigValueLayout { constructor(props: Props) { super(props); const valueWidthPercent = this.titleToAlignTo?.length ? 0.3 : 1.0; if (this.valueToAlignTo.length) { // initial value size this.valueFontSize = calculateFontSize( this.valueToAlignTo, this.maxTextWidth * valueWidthPercent, this.maxTextHeight, LINE_HEIGHT ); } if (this.titleToAlignTo?.length) { // How big can we make the title and still have it fit this.titleFontSize = calculateFontSize( this.titleToAlignTo, this.maxTextWidth * 0.6, this.maxTextHeight, LINE_HEIGHT, MAX_TITLE_SIZE ); // make sure it's a bit smaller than valueFontSize this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize); } } getValueAndTitleContainerStyles() { const styles = super.getValueAndTitleContainerStyles(); styles.flexDirection = 'row'; styles.alignItems = 'center'; styles.flexGrow = 1; if (!this.justifyCenter) { styles.justifyContent = 'space-between'; } return styles; } renderChart(): JSX.Element | null { return null; } getPanelStyles() { const panelStyles = super.getPanelStyles(); panelStyles.alignItems = 'center'; return panelStyles; } } export class WideWithChartLayout extends BigValueLayout { constructor(props: Props) { super(props); const { width, height } = props; const chartHeightPercent = 0.5; const titleWidthPercent = 0.6; const valueWidthPercent = 1 - titleWidthPercent; const textHeightPercent = 0.4; this.chartWidth = width; this.chartHeight = height * chartHeightPercent; if (this.titleToAlignTo?.length) { this.titleFontSize = calculateFontSize( this.titleToAlignTo, this.maxTextWidth * titleWidthPercent, this.maxTextHeight * textHeightPercent, LINE_HEIGHT, MAX_TITLE_SIZE ); } if (this.valueToAlignTo.length) { this.valueFontSize = calculateFontSize( this.valueToAlignTo, this.maxTextWidth * valueWidthPercent, this.maxTextHeight * chartHeightPercent, LINE_HEIGHT ); } } getValueAndTitleContainerStyles() { const styles = super.getValueAndTitleContainerStyles(); styles.flexDirection = 'row'; styles.flexGrow = 1; if (!this.justifyCenter) { styles.justifyContent = 'space-between'; } return styles; } getPanelStyles() { const styles = super.getPanelStyles(); styles.flexDirection = 'row'; styles.justifyContent = 'space-between'; return styles; } } export class StackedWithChartLayout extends BigValueLayout { constructor(props: Props) { super(props); const { width, height } = props; const titleHeightPercent = 0.15; const chartHeightPercent = 0.25; let titleHeight = 0; this.chartHeight = height * chartHeightPercent; this.chartWidth = width; if (this.titleToAlignTo?.length) { this.titleFontSize = calculateFontSize( this.titleToAlignTo, this.maxTextWidth, height * titleHeightPercent, LINE_HEIGHT, MAX_TITLE_SIZE ); } titleHeight = this.titleFontSize * LINE_HEIGHT; if (this.valueToAlignTo.length) { this.valueFontSize = calculateFontSize( this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight - this.chartHeight - titleHeight, LINE_HEIGHT ); } // make title fontsize it's a bit smaller than valueFontSize if (this.titleToAlignTo?.length) { this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize); } // make chart take up unused space this.chartHeight = height - this.titleFontSize * LINE_HEIGHT - this.valueFontSize * LINE_HEIGHT; } getValueAndTitleContainerStyles() { const styles = super.getValueAndTitleContainerStyles(); styles.flexDirection = 'column'; styles.justifyContent = 'center'; return styles; } getPanelStyles() { const styles = super.getPanelStyles(); styles.flexDirection = 'column'; return styles; } } export class StackedWithNoChartLayout extends BigValueLayout { constructor(props: Props) { super(props); const { height } = props; const titleHeightPercent = 0.15; let titleHeight = 0; if (this.titleToAlignTo?.length) { this.titleFontSize = calculateFontSize( this.titleToAlignTo, this.maxTextWidth, height * titleHeightPercent, LINE_HEIGHT, MAX_TITLE_SIZE ); titleHeight = this.titleFontSize * LINE_HEIGHT; } if (this.valueToAlignTo.length) { this.valueFontSize = calculateFontSize( this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight - titleHeight, LINE_HEIGHT ); } // make title fontsize it's a bit smaller than valueFontSize this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize); } getValueAndTitleContainerStyles() { const styles = super.getValueAndTitleContainerStyles(); styles.flexDirection = 'column'; styles.flexGrow = 1; return styles; } getPanelStyles() { const styles = super.getPanelStyles(); styles.alignItems = 'center'; return styles; } } export function buildLayout(props: Props): BigValueLayout { const { width, height, sparkline } = props; const useWideLayout = width / height > 2.5; if (useWideLayout) { if (height > 50 && !!sparkline) { return new WideWithChartLayout(props); } else { return new WideNoChartLayout(props); } } // stacked layouts if (height > 100 && !!sparkline) { return new StackedWithChartLayout(props); } else { return new StackedWithNoChartLayout(props); } } export function shouldJustifyCenter(justifyMode?: BigValueJustifyMode, title?: string) { if (justifyMode === BigValueJustifyMode.Center) { return true; } return (title ?? '').length === 0; } export interface BigValueTextValues extends DisplayValue { valueToAlignTo: string; titleToAlignTo?: string; tooltip?: string; } function getTextValues(props: Props): BigValueTextValues { const { value, alignmentFactors, count } = props; let { textMode } = props; const titleToAlignTo = alignmentFactors ? alignmentFactors.title : value.title; const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : value); // In the auto case we only show title if this big value is part of more panes (count > 1) if (textMode === BigValueTextMode.Auto && (count ?? 1) === 1) { textMode = BigValueTextMode.Value; } switch (textMode) { case BigValueTextMode.Name: return { ...value, title: undefined, prefix: undefined, suffix: undefined, text: value.title || '', titleToAlignTo: undefined, valueToAlignTo: titleToAlignTo ?? '', tooltip: formattedValueToString(value), }; case BigValueTextMode.Value: return { ...value, title: undefined, titleToAlignTo: undefined, valueToAlignTo, tooltip: value.title, }; case BigValueTextMode.None: return { numeric: value.numeric, color: value.color, title: undefined, text: '', titleToAlignTo: undefined, valueToAlignTo: '1', tooltip: `Name: ${value.title}\nValue: ${formattedValueToString(value)}`, }; case BigValueTextMode.ValueAndName: default: return { ...value, titleToAlignTo, valueToAlignTo, }; } }