mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
StatPanel: Refactoring & fixes (#21437)
* refactoring BigValue layout to use polymorphism * Renamed file * StatPanel: Fixed min & max handling, white theme background * Typescript and test fixes * Fixed justify center logic * Updated test dashboard * Updated test dashboard * Adjust time xMax to align with last data point if it's close * Fixed color issue * removed text shadow * Removed unused stuff * Fixed auto min max * Fixed strict typescript issues * Updated
This commit is contained in:
parent
4f0fa776be
commit
27a77f588c
@ -33,8 +33,6 @@
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
@ -57,7 +55,7 @@
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"unit": "percent"
|
||||
"unit": "areaM2"
|
||||
},
|
||||
"override": {},
|
||||
"values": false
|
||||
@ -124,8 +122,6 @@
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
@ -215,8 +211,6 @@
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
@ -239,7 +233,7 @@
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"unit": "percent"
|
||||
"unit": "areaM2"
|
||||
},
|
||||
"override": {},
|
||||
"values": false
|
||||
@ -300,8 +294,6 @@
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
@ -324,7 +316,7 @@
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"unit": "percent"
|
||||
"unit": "areaM2"
|
||||
},
|
||||
"override": {},
|
||||
"values": false
|
||||
@ -388,8 +380,6 @@
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
@ -412,7 +402,7 @@
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"unit": "percent"
|
||||
"unit": "areaM2"
|
||||
},
|
||||
"override": {},
|
||||
"values": false
|
||||
@ -476,8 +466,6 @@
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
@ -500,7 +488,7 @@
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"unit": "percent"
|
||||
"unit": "areaM2"
|
||||
},
|
||||
"override": {},
|
||||
"values": false
|
||||
|
@ -4,21 +4,15 @@ import { DisplayValue, GraphSeriesValue, DisplayValueAlignmentFactors } from '@g
|
||||
|
||||
// Types
|
||||
import { Themeable } from '../../types';
|
||||
import {
|
||||
calculateLayout,
|
||||
getPanelStyles,
|
||||
getValueAndTitleContainerStyles,
|
||||
getValueStyles,
|
||||
getTitleStyles,
|
||||
} from './styles';
|
||||
|
||||
import { renderGraph } from './renderGraph';
|
||||
import { buildLayout } from './BigValueLayout';
|
||||
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
|
||||
|
||||
export interface BigValueSparkline {
|
||||
data: GraphSeriesValue[][];
|
||||
minX: number;
|
||||
maxX: number;
|
||||
xMin?: number | null;
|
||||
xMax?: number | null;
|
||||
yMin?: number | null;
|
||||
yMax?: number | null;
|
||||
highlightIndex?: number;
|
||||
}
|
||||
|
||||
@ -57,13 +51,13 @@ export class BigValue extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, onClick, className, sparkline } = this.props;
|
||||
const { value, onClick, className } = this.props;
|
||||
|
||||
const layout = calculateLayout(this.props);
|
||||
const panelStyles = getPanelStyles(layout);
|
||||
const valueAndTitleContainerStyles = getValueAndTitleContainerStyles(layout);
|
||||
const valueStyles = getValueStyles(layout);
|
||||
const titleStyles = getTitleStyles(layout);
|
||||
const layout = buildLayout(this.props);
|
||||
const panelStyles = layout.getPanelStyles();
|
||||
const valueAndTitleContainerStyles = layout.getValueAndTitleContainerStyles();
|
||||
const valueStyles = layout.getValueStyles();
|
||||
const titleStyles = layout.getTitleStyles();
|
||||
|
||||
return (
|
||||
<div className={className} style={panelStyles} onClick={onClick}>
|
||||
@ -71,7 +65,7 @@ export class BigValue extends PureComponent<Props> {
|
||||
{value.title && <div style={titleStyles}>{value.title}</div>}
|
||||
<FormattedValueDisplay value={value} style={valueStyles} />
|
||||
</div>
|
||||
{renderGraph(layout, sparkline)}
|
||||
{layout.renderChart()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
|
||||
import { calculateLayout, LayoutType } from './styles';
|
||||
import { buildLayout, StackedWithChartLayout, WideWithChartLayout } from './BigValueLayout';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
function getProps(propOverrides?: Partial<Props>): Props {
|
||||
@ -17,8 +17,8 @@ function getProps(propOverrides?: Partial<Props>): Props {
|
||||
[10, 10],
|
||||
[10, 10],
|
||||
],
|
||||
minX: 0,
|
||||
maxX: 100,
|
||||
xMin: 0,
|
||||
xMax: 100,
|
||||
},
|
||||
theme: getTheme(),
|
||||
};
|
||||
@ -27,26 +27,26 @@ function getProps(propOverrides?: Partial<Props>): Props {
|
||||
return props;
|
||||
}
|
||||
|
||||
describe('BigValue styles', () => {
|
||||
describe('calculateLayout', () => {
|
||||
describe('BigValueLayout', () => {
|
||||
describe('buildLayout', () => {
|
||||
it('should auto select to stacked layout', () => {
|
||||
const layout = calculateLayout(
|
||||
const layout = buildLayout(
|
||||
getProps({
|
||||
width: 300,
|
||||
height: 300,
|
||||
})
|
||||
);
|
||||
expect(layout.type).toBe(LayoutType.Stacked);
|
||||
expect(layout).toBeInstanceOf(StackedWithChartLayout);
|
||||
});
|
||||
|
||||
it('should auto select to wide layout', () => {
|
||||
const layout = calculateLayout(
|
||||
const layout = buildLayout(
|
||||
getProps({
|
||||
width: 300,
|
||||
height: 100,
|
||||
})
|
||||
);
|
||||
expect(layout.type).toBe(LayoutType.Wide);
|
||||
expect(layout).toBeInstanceOf(WideWithChartLayout);
|
||||
});
|
||||
});
|
||||
});
|
453
packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx
Normal file
453
packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx
Normal file
@ -0,0 +1,453 @@
|
||||
// Libraries
|
||||
import React, { CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { Chart, Geom } from 'bizcharts';
|
||||
|
||||
// Utils
|
||||
import { getColorFromHexRgbOrName, formattedValueToString } from '@grafana/data';
|
||||
import { calculateFontSize } from '../../utils/measureText';
|
||||
|
||||
// Types
|
||||
import { BigValueColorMode, Props, BigValueJustifyMode } from './BigValue';
|
||||
|
||||
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;
|
||||
|
||||
constructor(private props: Props) {
|
||||
const { width, height, value, alignmentFactors, theme } = props;
|
||||
|
||||
this.valueColor = getColorFromHexRgbOrName(value.color || 'green', theme.type);
|
||||
this.justifyCenter = shouldJustifyCenter(props);
|
||||
this.panelPadding = height > 100 ? 12 : 8;
|
||||
this.titleToAlignTo = alignmentFactors ? alignmentFactors.title : value.title;
|
||||
this.valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : value);
|
||||
|
||||
this.titleFontSize = 14;
|
||||
this.valueFontSize = 14;
|
||||
this.chartHeight = 0;
|
||||
this.chartWidth = 0;
|
||||
this.maxTextWidth = width - this.panelPadding * 2;
|
||||
this.maxTextHeight = height - this.panelPadding * 2;
|
||||
}
|
||||
|
||||
getTitleStyles(): CSSProperties {
|
||||
const styles: CSSProperties = {
|
||||
fontSize: `${this.titleFontSize}px`,
|
||||
lineHeight: LINE_HEIGHT,
|
||||
};
|
||||
|
||||
if (this.props.colorMode === BigValueColorMode.Background) {
|
||||
styles.color = 'white';
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
getValueStyles(): CSSProperties {
|
||||
const styles: CSSProperties = {
|
||||
fontSize: this.valueFontSize,
|
||||
fontWeight: 500,
|
||||
lineHeight: LINE_HEIGHT,
|
||||
};
|
||||
|
||||
switch (this.props.colorMode) {
|
||||
case BigValueColorMode.Value:
|
||||
styles.color = this.valueColor;
|
||||
break;
|
||||
case BigValueColorMode.Background:
|
||||
styles.color = 'white';
|
||||
}
|
||||
|
||||
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 = `${theme.colors.panelBg}`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.justifyCenter) {
|
||||
panelStyles.alignItems = 'center';
|
||||
panelStyles.flexDirection = 'row';
|
||||
}
|
||||
|
||||
return panelStyles;
|
||||
}
|
||||
|
||||
renderChart(): JSX.Element | null {
|
||||
const { sparkline } = this.props;
|
||||
|
||||
if (!sparkline || sparkline.data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = sparkline.data.map(values => {
|
||||
return { time: values[0], value: values[1], name: 'A' };
|
||||
});
|
||||
|
||||
const scales = {
|
||||
time: {
|
||||
type: 'time',
|
||||
min: sparkline.xMin,
|
||||
max: sparkline.xMax,
|
||||
},
|
||||
value: {
|
||||
min: sparkline.yMin,
|
||||
max: sparkline.yMax,
|
||||
},
|
||||
};
|
||||
|
||||
if (sparkline.xMax && sparkline.xMin) {
|
||||
// Having the last data point align with the edge of the panel looks good
|
||||
// So if it's close adjust time.max to the last data point time
|
||||
const timeDelta = sparkline.xMax - sparkline.xMin;
|
||||
const lastDataPointTime = data[data.length - 1].time || 0;
|
||||
const lastTimeDiffFromMax = Math.abs(sparkline.xMax - lastDataPointTime);
|
||||
|
||||
// if last data point is just 5% or lower from the edge adjust it
|
||||
if (lastTimeDiffFromMax / timeDelta < 0.05) {
|
||||
scales.time.max = lastDataPointTime;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Chart
|
||||
height={this.chartHeight}
|
||||
width={this.chartWidth}
|
||||
data={data}
|
||||
animate={false}
|
||||
padding={[4, 0, 0, 0]}
|
||||
scale={scales}
|
||||
style={this.getChartStyles()}
|
||||
>
|
||||
{this.renderGeom()}
|
||||
</Chart>
|
||||
);
|
||||
}
|
||||
|
||||
renderGeom(): JSX.Element {
|
||||
const { colorMode } = this.props;
|
||||
|
||||
const lineStyle: any = {
|
||||
opacity: 1,
|
||||
fillOpacity: 1,
|
||||
lineWidth: 2,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
lineStyle.stroke = lineColor;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Geom type="area" position="time*value" size={0} color={fillColor} style={lineStyle} shape="smooth" />
|
||||
<Geom type="line" position="time*value" size={1} color={lineColor} style={lineStyle} shape="smooth" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
getChartStyles(): CSSProperties {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class WideNoChartLayout extends BigValueLayout {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const valueWidthPercent = 0.3;
|
||||
|
||||
if (this.titleToAlignTo && this.titleToAlignTo.length > 0) {
|
||||
// initial value size
|
||||
this.valueFontSize = calculateFontSize(
|
||||
this.valueToAlignTo,
|
||||
this.maxTextWidth * valueWidthPercent,
|
||||
this.maxTextHeight,
|
||||
LINE_HEIGHT
|
||||
);
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
// if no title wide
|
||||
this.valueFontSize = calculateFontSize(this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight, LINE_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
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 && this.titleToAlignTo.length > 0) {
|
||||
this.titleFontSize = calculateFontSize(
|
||||
this.titleToAlignTo,
|
||||
this.maxTextWidth * titleWidthPercent,
|
||||
this.maxTextHeight * textHeightPercent,
|
||||
LINE_HEIGHT,
|
||||
MAX_TITLE_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
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 && this.titleToAlignTo.length > 0) {
|
||||
this.titleFontSize = calculateFontSize(
|
||||
this.titleToAlignTo,
|
||||
this.maxTextWidth,
|
||||
height * titleHeightPercent,
|
||||
LINE_HEIGHT,
|
||||
MAX_TITLE_SIZE
|
||||
);
|
||||
|
||||
titleHeight = this.titleFontSize * LINE_HEIGHT;
|
||||
}
|
||||
|
||||
this.valueFontSize = calculateFontSize(
|
||||
this.valueToAlignTo,
|
||||
this.maxTextWidth,
|
||||
this.maxTextHeight - this.chartHeight - 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.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 && this.titleToAlignTo.length > 0) {
|
||||
this.titleFontSize = calculateFontSize(
|
||||
this.titleToAlignTo,
|
||||
this.maxTextWidth,
|
||||
height * titleHeightPercent,
|
||||
LINE_HEIGHT,
|
||||
MAX_TITLE_SIZE
|
||||
);
|
||||
|
||||
titleHeight = this.titleFontSize * LINE_HEIGHT;
|
||||
}
|
||||
|
||||
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(props: Props) {
|
||||
const { value, justifyMode } = props;
|
||||
if (justifyMode === BigValueJustifyMode.Center) {
|
||||
return true;
|
||||
}
|
||||
return (value.title ?? '').length === 0;
|
||||
}
|
@ -30,11 +30,10 @@ exports[`BigValue Render with basic options should render 1`] = `
|
||||
<FormattedDisplayValue
|
||||
style={
|
||||
Object {
|
||||
"color": "#EEE",
|
||||
"color": "white",
|
||||
"fontSize": 230,
|
||||
"fontWeight": 500,
|
||||
"lineHeight": 1.2,
|
||||
"textShadow": "#333 0px 0px 1px",
|
||||
}
|
||||
}
|
||||
value={
|
||||
|
@ -1,151 +0,0 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { Chart, Geom, Guide } from 'bizcharts';
|
||||
import { LayoutResult, LayoutType } from './styles';
|
||||
import { BigValueSparkline, BigValueColorMode } from './BigValue';
|
||||
|
||||
const { DataMarker } = Guide;
|
||||
|
||||
export function renderGraph(layout: LayoutResult, sparkline?: BigValueSparkline) {
|
||||
if (!sparkline || layout.type === LayoutType.WideNoChart || layout.type === LayoutType.StackedNoChart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = sparkline.data.map(values => {
|
||||
return { time: values[0], value: values[1], name: 'A' };
|
||||
});
|
||||
|
||||
const scales = {
|
||||
time: {
|
||||
type: 'time',
|
||||
},
|
||||
};
|
||||
|
||||
const chartStyles: CSSProperties = {
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
// default to line graph
|
||||
const geomRender = getGraphGeom(layout.colorMode);
|
||||
|
||||
if (layout.type === LayoutType.Wide) {
|
||||
// Area chart
|
||||
chartStyles.bottom = 0;
|
||||
chartStyles.right = 0;
|
||||
} else {
|
||||
// need some top padding
|
||||
chartStyles.width = `${layout.chartWidth}px`;
|
||||
chartStyles.height = `${layout.chartHeight}px`;
|
||||
chartStyles.bottom = 0;
|
||||
chartStyles.right = 0;
|
||||
chartStyles.left = 0;
|
||||
chartStyles.right = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chart
|
||||
height={layout.chartHeight}
|
||||
width={layout.chartWidth}
|
||||
data={data}
|
||||
animate={false}
|
||||
padding={[4, 0, 0, 0]}
|
||||
scale={scales}
|
||||
style={chartStyles}
|
||||
>
|
||||
{geomRender(layout, sparkline)}
|
||||
</Chart>
|
||||
);
|
||||
}
|
||||
function getGraphGeom(colorMode: BigValueColorMode) {
|
||||
// background color mode
|
||||
if (colorMode === BigValueColorMode.Background) {
|
||||
return renderAreaGeomOnColoredBackground;
|
||||
}
|
||||
return renderClassicAreaGeom;
|
||||
}
|
||||
|
||||
function renderAreaGeomOnColoredBackground(layout: LayoutResult, sparkline: BigValueSparkline) {
|
||||
const lineColor = tinycolor(layout.valueColor)
|
||||
.brighten(40)
|
||||
.toRgbString();
|
||||
|
||||
const lineStyle: any = {
|
||||
stroke: lineColor,
|
||||
lineWidth: 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Geom type="area" position="time*value" size={0} color="rgba(255,255,255,0.4)" style={lineStyle} shape="smooth" />
|
||||
<Geom type="line" position="time*value" size={1} color={lineColor} style={lineStyle} shape="smooth" />
|
||||
{highlightPoint(lineColor, sparkline)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function highlightPoint(lineColor: string, sparkline: BigValueSparkline) {
|
||||
if (!sparkline.highlightIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pointPos = sparkline.data[sparkline.highlightIndex];
|
||||
|
||||
return (
|
||||
<Guide>
|
||||
<DataMarker
|
||||
top
|
||||
position={pointPos}
|
||||
lineLength={0}
|
||||
display={{ point: true }}
|
||||
style={{
|
||||
point: {
|
||||
color: lineColor,
|
||||
stroke: lineColor,
|
||||
r: 2,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Guide>
|
||||
);
|
||||
}
|
||||
|
||||
function renderClassicAreaGeom(layout: LayoutResult, sparkline: BigValueSparkline) {
|
||||
const lineStyle: any = {
|
||||
opacity: 1,
|
||||
fillOpacity: 1,
|
||||
};
|
||||
|
||||
const fillColor = tinycolor(layout.valueColor)
|
||||
.setAlpha(0.2)
|
||||
.toRgbString();
|
||||
lineStyle.stroke = layout.valueColor;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Geom type="area" position="time*value" size={0} color={fillColor} style={lineStyle} shape="smooth" />
|
||||
<Geom type="line" position="time*value" size={1} color={layout.valueColor} style={lineStyle} shape="smooth" />
|
||||
{highlightPoint('#EEE', sparkline)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* function renderAreaGeom(layout: LayoutResult) { */
|
||||
/* const lineStyle: any = { */
|
||||
/* opacity: 1, */
|
||||
/* fillOpacity: 1, */
|
||||
/* }; */
|
||||
/* */
|
||||
/* const color1 = tinycolor(layout.valueColor) */
|
||||
/* .darken(0) */
|
||||
/* .spin(20) */
|
||||
/* .toRgbString(); */
|
||||
/* const color2 = tinycolor(layout.valueColor) */
|
||||
/* .lighten(0) */
|
||||
/* .spin(-20) */
|
||||
/* .toRgbString(); */
|
||||
/* */
|
||||
/* const fillColor = `l (0) 0:${color1} 1:${color2}`; */
|
||||
/* */
|
||||
/* return <Geom type="area" position="time*value" size={0} color={fillColor} style={lineStyle} shape="smooth" />; */
|
||||
/* } */
|
@ -1,290 +0,0 @@
|
||||
// Libraries
|
||||
import { CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Utils
|
||||
import { getColorFromHexRgbOrName, GrafanaTheme, formattedValueToString } from '@grafana/data';
|
||||
import { calculateFontSize } from '../../utils/measureText';
|
||||
|
||||
// Types
|
||||
import { BigValueColorMode, BigValueGraphMode, Props, BigValueJustifyMode } from './BigValue';
|
||||
|
||||
const LINE_HEIGHT = 1.2;
|
||||
|
||||
export interface LayoutResult {
|
||||
titleFontSize: number;
|
||||
valueFontSize: number;
|
||||
chartHeight: number;
|
||||
chartWidth: number;
|
||||
type: LayoutType;
|
||||
width: number;
|
||||
height: number;
|
||||
colorMode: BigValueColorMode;
|
||||
graphMode: BigValueGraphMode;
|
||||
theme: GrafanaTheme;
|
||||
valueColor: string;
|
||||
panelPadding: number;
|
||||
justifyCenter: boolean;
|
||||
}
|
||||
|
||||
export enum LayoutType {
|
||||
Stacked,
|
||||
StackedNoChart,
|
||||
Wide,
|
||||
WideNoChart,
|
||||
}
|
||||
|
||||
export function shouldJustifyCenter(props: Props) {
|
||||
const { value, justifyMode } = props;
|
||||
if (justifyMode === BigValueJustifyMode.Center) {
|
||||
return true;
|
||||
}
|
||||
return (value.title ?? '').length === 0;
|
||||
}
|
||||
|
||||
export function calculateLayout(props: Props): LayoutResult {
|
||||
const { width, height, sparkline, colorMode, theme, value, graphMode, alignmentFactors } = props;
|
||||
const useWideLayout = width / height > 2.5;
|
||||
const valueColor = getColorFromHexRgbOrName(value.color || 'green', theme.type);
|
||||
const justifyCenter = shouldJustifyCenter(props);
|
||||
const panelPadding = height > 100 ? 12 : 8;
|
||||
const titleToAlignTo = alignmentFactors ? alignmentFactors.title : value.title;
|
||||
const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : value);
|
||||
|
||||
const maxTitleFontSize = 30;
|
||||
const maxTextWidth = width - panelPadding * 2;
|
||||
const maxTextHeight = height - panelPadding * 2;
|
||||
|
||||
let layoutType = LayoutType.Stacked;
|
||||
let chartHeight = 0;
|
||||
let chartWidth = 0;
|
||||
let titleHeight = 0;
|
||||
let titleFontSize = 0;
|
||||
let valueFontSize = 14;
|
||||
|
||||
if (useWideLayout) {
|
||||
// Detect auto wide layout type
|
||||
layoutType = height > 50 && !!sparkline ? LayoutType.Wide : LayoutType.WideNoChart;
|
||||
|
||||
// Wide no chart mode
|
||||
if (layoutType === LayoutType.WideNoChart) {
|
||||
const valueWidthPercent = 0.3;
|
||||
|
||||
if (titleToAlignTo && titleToAlignTo.length > 0) {
|
||||
// initial value size
|
||||
valueFontSize = calculateFontSize(valueToAlignTo, maxTextWidth * valueWidthPercent, maxTextHeight, LINE_HEIGHT);
|
||||
// How big can we make the title and still have it fit
|
||||
titleFontSize = calculateFontSize(
|
||||
titleToAlignTo,
|
||||
maxTextWidth * 0.6,
|
||||
maxTextHeight,
|
||||
LINE_HEIGHT,
|
||||
maxTitleFontSize
|
||||
);
|
||||
|
||||
// make sure it's a bit smaller than valueFontSize
|
||||
titleFontSize = Math.min(valueFontSize * 0.7, titleFontSize);
|
||||
titleHeight = titleFontSize * LINE_HEIGHT;
|
||||
} else {
|
||||
// if no title wide
|
||||
valueFontSize = calculateFontSize(valueToAlignTo, maxTextWidth, maxTextHeight, LINE_HEIGHT);
|
||||
}
|
||||
} else {
|
||||
// wide with chart
|
||||
const chartHeightPercent = 0.5;
|
||||
const titleWidthPercent = 0.6;
|
||||
const valueWidthPercent = 1 - titleWidthPercent;
|
||||
const textHeightPercent = 0.4;
|
||||
|
||||
chartWidth = width;
|
||||
chartHeight = height * chartHeightPercent;
|
||||
|
||||
if (titleToAlignTo && titleToAlignTo.length > 0) {
|
||||
titleFontSize = calculateFontSize(
|
||||
titleToAlignTo,
|
||||
maxTextWidth * titleWidthPercent,
|
||||
maxTextHeight * textHeightPercent,
|
||||
LINE_HEIGHT,
|
||||
maxTitleFontSize
|
||||
);
|
||||
titleHeight = titleFontSize * LINE_HEIGHT;
|
||||
}
|
||||
|
||||
valueFontSize = calculateFontSize(
|
||||
valueToAlignTo,
|
||||
maxTextWidth * valueWidthPercent,
|
||||
maxTextHeight * chartHeightPercent,
|
||||
LINE_HEIGHT
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Stacked layout (title, value, chart)
|
||||
const titleHeightPercent = 0.15;
|
||||
const chartHeightPercent = 0.25;
|
||||
|
||||
// Does a chart fit or exist?
|
||||
if (height < 100 || !sparkline) {
|
||||
layoutType = LayoutType.StackedNoChart;
|
||||
} else {
|
||||
// we have chart
|
||||
chartHeight = height * chartHeightPercent;
|
||||
chartWidth = width;
|
||||
}
|
||||
|
||||
if (titleToAlignTo && titleToAlignTo.length > 0) {
|
||||
titleFontSize = calculateFontSize(
|
||||
titleToAlignTo,
|
||||
maxTextWidth,
|
||||
height * titleHeightPercent,
|
||||
LINE_HEIGHT,
|
||||
maxTitleFontSize
|
||||
);
|
||||
titleHeight = titleFontSize * LINE_HEIGHT;
|
||||
}
|
||||
|
||||
valueFontSize = calculateFontSize(
|
||||
valueToAlignTo,
|
||||
maxTextWidth,
|
||||
maxTextHeight - chartHeight - titleHeight,
|
||||
LINE_HEIGHT
|
||||
);
|
||||
// make title fontsize it's a bit smaller than valueFontSize
|
||||
titleFontSize = Math.min(valueFontSize * 0.7, titleFontSize);
|
||||
}
|
||||
|
||||
return {
|
||||
valueFontSize,
|
||||
titleFontSize,
|
||||
chartHeight,
|
||||
chartWidth,
|
||||
type: layoutType,
|
||||
width,
|
||||
height,
|
||||
colorMode,
|
||||
graphMode,
|
||||
theme,
|
||||
valueColor,
|
||||
justifyCenter,
|
||||
panelPadding,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTitleStyles(layout: LayoutResult) {
|
||||
const styles: CSSProperties = {
|
||||
fontSize: `${layout.titleFontSize}px`,
|
||||
textShadow: '#333 0px 0px 1px',
|
||||
color: '#EEE',
|
||||
lineHeight: LINE_HEIGHT,
|
||||
};
|
||||
|
||||
if (layout.theme.isLight) {
|
||||
styles.color = 'white';
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
export function getValueStyles(layout: LayoutResult) {
|
||||
const styles: CSSProperties = {
|
||||
fontSize: layout.valueFontSize,
|
||||
color: '#EEE',
|
||||
textShadow: '#333 0px 0px 1px',
|
||||
fontWeight: 500,
|
||||
lineHeight: LINE_HEIGHT,
|
||||
};
|
||||
|
||||
switch (layout.colorMode) {
|
||||
case BigValueColorMode.Value:
|
||||
styles.color = layout.valueColor;
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
export function getValueAndTitleContainerStyles(layout: LayoutResult): CSSProperties {
|
||||
const styles: CSSProperties = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
switch (layout.type) {
|
||||
case LayoutType.Wide:
|
||||
styles.flexDirection = 'row';
|
||||
styles.justifyContent = 'space-between';
|
||||
styles.flexGrow = 1;
|
||||
break;
|
||||
case LayoutType.WideNoChart:
|
||||
styles.flexDirection = 'row';
|
||||
styles.justifyContent = 'space-between';
|
||||
styles.alignItems = 'center';
|
||||
styles.flexGrow = 1;
|
||||
break;
|
||||
case LayoutType.StackedNoChart:
|
||||
styles.flexDirection = 'column';
|
||||
styles.flexGrow = 1;
|
||||
break;
|
||||
case LayoutType.Stacked:
|
||||
default:
|
||||
styles.flexDirection = 'column';
|
||||
styles.justifyContent = 'center';
|
||||
}
|
||||
|
||||
if (layout.justifyCenter) {
|
||||
styles.alignItems = 'center';
|
||||
styles.justifyContent = 'center';
|
||||
styles.flexGrow = 1;
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
export function getPanelStyles(layout: LayoutResult) {
|
||||
const panelStyles: CSSProperties = {
|
||||
width: `${layout.width}px`,
|
||||
height: `${layout.height}px`,
|
||||
padding: `${layout.panelPadding}px`,
|
||||
borderRadius: '3px',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const themeFactor = layout.theme.isDark ? 1 : -0.7;
|
||||
|
||||
switch (layout.colorMode) {
|
||||
case BigValueColorMode.Background:
|
||||
const bgColor2 = tinycolor(layout.valueColor)
|
||||
.darken(15 * themeFactor)
|
||||
.spin(8)
|
||||
.toRgbString();
|
||||
const bgColor3 = tinycolor(layout.valueColor)
|
||||
.darken(5 * themeFactor)
|
||||
.spin(-8)
|
||||
.toRgbString();
|
||||
panelStyles.background = `linear-gradient(120deg, ${bgColor2}, ${bgColor3})`;
|
||||
break;
|
||||
case BigValueColorMode.Value:
|
||||
panelStyles.background = `${layout.theme.colors.dark4}`;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (layout.type) {
|
||||
case LayoutType.Stacked:
|
||||
panelStyles.flexDirection = 'column';
|
||||
break;
|
||||
case LayoutType.StackedNoChart:
|
||||
panelStyles.alignItems = 'center';
|
||||
break;
|
||||
case LayoutType.Wide:
|
||||
panelStyles.flexDirection = 'row';
|
||||
panelStyles.justifyContent = 'space-between';
|
||||
break;
|
||||
case LayoutType.WideNoChart:
|
||||
panelStyles.alignItems = 'center';
|
||||
break;
|
||||
}
|
||||
|
||||
if (layout.justifyCenter) {
|
||||
panelStyles.alignItems = 'center';
|
||||
panelStyles.flexDirection = 'row';
|
||||
}
|
||||
|
||||
return panelStyles;
|
||||
}
|
@ -33,8 +33,10 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
|
||||
if (value.sparkline) {
|
||||
sparkline = {
|
||||
data: value.sparkline,
|
||||
minX: timeRange.from.valueOf(),
|
||||
maxX: timeRange.to.valueOf(),
|
||||
xMin: timeRange.from.valueOf(),
|
||||
xMax: timeRange.to.valueOf(),
|
||||
yMin: value.field.min,
|
||||
yMax: value.field.max,
|
||||
};
|
||||
|
||||
const calc = options.fieldOptions.calcs[0];
|
||||
@ -75,6 +77,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
|
||||
theme: config.theme,
|
||||
data: data.series,
|
||||
sparkline: options.graphMode !== BigValueGraphMode.None,
|
||||
autoMinMax: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user