Bar Gauge: Show tile (series name) & refactorings & tests (#16397)

* WIP: began work on adding title support to bar gauge

* Feat: BarGauge progress

* wip: trying improve text size handling in bar gauge

* BarGauge: progress on title & value auto sizing

* BarGauge: more auto size handling

* bargauge: minor tweaks

* Added tests

* Refactoring: BarGauge refactoring moving css generation to seperate functions and adding some basic tests

* Refactoring VizRepeater and more

* Fix: updated and fixed tests
This commit is contained in:
Torkel Ödegaard 2019-04-05 12:59:29 +02:00 committed by GitHub
parent 9530906822
commit 566b3d178a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 625 additions and 326 deletions

View File

@ -1,61 +1,82 @@
import { storiesOf } from '@storybook/react';
import { number, text, boolean } from '@storybook/addon-knobs';
import { BarGauge } from './BarGauge';
import { number, text } from '@storybook/addon-knobs';
import { BarGauge, Props } from './BarGauge';
import { VizOrientation } from '../../types';
import { withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
const getKnobs = () => {
return {
value: number('value', 70),
title: text('title', 'Title'),
minValue: number('minValue', 0),
maxValue: number('maxValue', 100),
threshold1Value: number('threshold1Value', 40),
threshold1Color: text('threshold1Color', 'orange'),
threshold2Value: number('threshold2Value', 60),
threshold2Color: text('threshold2Color', 'red'),
unit: text('unit', 'ms'),
decimals: number('decimals', 1),
horizontal: boolean('horizontal', false),
lcd: boolean('lcd', false),
};
};
const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
BarGaugeStories.addDecorator(withHorizontallyCenteredStory);
BarGaugeStories.addDecorator(withCenteredStory);
BarGaugeStories.add('Simple with basic thresholds', () => {
const {
value,
minValue,
maxValue,
threshold1Color,
threshold2Color,
threshold1Value,
threshold2Value,
unit,
decimals,
horizontal,
lcd,
} = getKnobs();
function addBarGaugeStory(name: string, overrides: Partial<Props>) {
BarGaugeStories.add(name, () => {
const {
value,
title,
minValue,
maxValue,
threshold1Color,
threshold2Color,
threshold1Value,
threshold2Value,
} = getKnobs();
return renderComponentWithTheme(BarGauge, {
width: 300,
height: 300,
value: { text: value.toString(), numeric: value },
minValue: minValue,
maxValue: maxValue,
unit: unit,
prefix: '',
postfix: '',
decimals: decimals,
orientation: horizontal ? VizOrientation.Horizontal : VizOrientation.Vertical,
displayMode: lcd ? 'lcd' : 'simple',
thresholds: [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: threshold1Value, color: threshold1Color },
{ index: 1, value: threshold2Value, color: threshold2Color },
],
const props: Props = {
theme: {} as any,
width: 300,
height: 300,
value: {
text: value.toString(),
title: title,
numeric: value,
},
minValue: minValue,
maxValue: maxValue,
orientation: VizOrientation.Vertical,
displayMode: 'basic',
thresholds: [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: threshold1Value, color: threshold1Color },
{ index: 1, value: threshold2Value, color: threshold2Color },
],
};
Object.assign(props, overrides);
return renderComponentWithTheme(BarGauge, props);
});
}
addBarGaugeStory('Gradient Vertical', {
displayMode: 'gradient',
orientation: VizOrientation.Vertical,
height: 500,
width: 100,
});
addBarGaugeStory('Gradient Horizontal', {
displayMode: 'gradient',
orientation: VizOrientation.Horizontal,
height: 100,
width: 500,
});
addBarGaugeStory('LCD Horizontal', {
displayMode: 'lcd',
orientation: VizOrientation.Vertical,
height: 500,
width: 100,
});

View File

@ -1,19 +1,27 @@
import React from 'react';
import { shallow } from 'enzyme';
import { BarGauge, Props } from './BarGauge';
import { VizOrientation } from '../../types';
import { BarGauge, Props, getValueColor, getBasicAndGradientStyles, getBarGradient, getTitleStyles } from './BarGauge';
import { VizOrientation, DisplayValue } from '../../types';
import { getTheme } from '../../themes';
jest.mock('jquery', () => ({
plot: jest.fn(),
}));
// jest.mock('jquery', () => ({
// plot: jest.fn(),
// }));
const setup = (propOverrides?: object) => {
const green = '#73BF69';
const orange = '#FF9830';
// const red = '#BB';
function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
maxValue: 100,
minValue: 0,
displayMode: 'basic',
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
thresholds: [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: 70, color: 'orange' },
{ index: 2, value: 90, color: 'red' },
],
height: 300,
width: 300,
value: {
@ -25,7 +33,11 @@ const setup = (propOverrides?: object) => {
};
Object.assign(props, propOverrides);
return props;
}
const setup = (propOverrides?: object) => {
const props = getProps(propOverrides);
const wrapper = shallow(<BarGauge {...props} />);
const instance = wrapper.instance() as BarGauge;
@ -35,29 +47,88 @@ const setup = (propOverrides?: object) => {
};
};
describe('Get font color', () => {
it('should get first threshold color when only one threshold', () => {
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
function getValue(value: number, title?: string): DisplayValue {
return { numeric: value, text: value.toString(), title: title };
}
expect(instance.getValueColors().value).toEqual('#7EB26D');
describe('BarGauge', () => {
describe('Get value color', () => {
it('should get the threshold color if value is same as a threshold', () => {
const props = getProps({ value: getValue(70) });
expect(getValueColor(props)).toEqual(orange);
});
it('should get the base threshold', () => {
const props = getProps({ value: getValue(-10) });
expect(getValueColor(props)).toEqual(green);
});
});
it('should get the threshold color if value is same as a threshold', () => {
const { instance } = setup({
thresholds: [
{ index: 2, value: 75, color: '#6ED0E0' },
{ index: 1, value: 10, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
],
describe('Vertical bar without title', () => {
it('should not include title height in height', () => {
const props = getProps({
height: 300,
value: getValue(100),
orientation: VizOrientation.Vertical,
});
const styles = getBasicAndGradientStyles(props);
expect(styles.bar.height).toBe('270px');
});
});
describe('Vertical bar with title', () => {
it('should include title height in height', () => {
const props = getProps({
height: 300,
value: getValue(100, 'ServerA'),
orientation: VizOrientation.Vertical,
});
const styles = getBasicAndGradientStyles(props);
expect(styles.bar.height).toBe('249px');
});
});
describe('Horizontal bar with title', () => {
it('should place above if height > 40', () => {
const props = getProps({
height: 41,
value: getValue(100, 'AA'),
orientation: VizOrientation.Horizontal,
});
const styles = getTitleStyles(props);
expect(styles.wrapper.flexDirection).toBe('column');
});
});
describe('Horizontal bar with title', () => {
it('should place below if height < 40', () => {
const props = getProps({
height: 30,
value: getValue(100, 'AA'),
orientation: VizOrientation.Horizontal,
});
const styles = getTitleStyles(props);
expect(styles.wrapper.flexDirection).toBe('row');
});
});
describe('Gradient', () => {
it('should build gradient based on thresholds', () => {
const props = getProps({ orientation: VizOrientation.Vertical, value: getValue(100) });
const gradient = getBarGradient(props, 300);
expect(gradient).toBe('linear-gradient(0deg, #73BF69, #73BF69 105px, #FF9830 240px, #F2495C)');
});
expect(instance.getValueColors().value).toEqual('#EAB839');
it('should stop gradient if value < threshold', () => {
const props = getProps({ orientation: VizOrientation.Vertical, value: getValue(70) });
const gradient = getBarGradient(props, 300);
expect(gradient).toBe('linear-gradient(0deg, #73BF69, #73BF69 105px, #FF9830)');
});
});
});
describe('Render BarGauge with basic options', () => {
it('should render', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
describe('Render with basic options', () => {
it('should render', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@ -8,7 +8,11 @@ import { getColorFromHexRgbOrName, getThresholdForValue } from '../../utils';
// Types
import { DisplayValue, Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
const BAR_SIZE_RATIO = 0.8;
const MIN_VALUE_HEIGHT = 18;
const MAX_VALUE_HEIGHT = 50;
const MIN_VALUE_WIDTH = 50;
const MAX_VALUE_WIDTH = 100;
const LINE_HEIGHT = 1.5;
export interface Props extends Themeable {
height: number;
@ -18,6 +22,7 @@ export interface Props extends Themeable {
maxValue: number;
minValue: number;
orientation: VizOrientation;
itemSpacing?: number;
displayMode: 'basic' | 'lcd' | 'gradient';
}
@ -32,9 +37,27 @@ export class BarGauge extends PureComponent<Props> {
displayMode: 'lcd',
orientation: VizOrientation.Horizontal,
thresholds: [],
itemSpacing: 10,
};
render() {
const { title } = this.props.value;
if (!title) {
return this.renderBarAndValue();
}
const styles = getTitleStyles(this.props);
return (
<div style={styles.wrapper}>
<div style={styles.title}>{title}</div>
{this.renderBarAndValue()}
</div>
);
}
renderBarAndValue() {
switch (this.props.displayMode) {
case 'lcd':
return this.renderRetroBars();
@ -45,143 +68,17 @@ export class BarGauge extends PureComponent<Props> {
}
}
getValueColors(): BarColors {
const { thresholds, theme, value } = this.props;
const activeThreshold = getThresholdForValue(thresholds, value.numeric);
if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
return {
value: color,
border: color,
background: tinycolor(color)
.setAlpha(0.15)
.toRgbString(),
};
}
return {
value: getColorFromHexRgbOrName('gray', theme.type),
background: getColorFromHexRgbOrName('gray', theme.type),
border: getColorFromHexRgbOrName('gray', theme.type),
};
}
getValueStyles(value: string, color: string, width: number): CSSProperties {
const guess = width / (value.length * 1.1);
const fontSize = Math.min(Math.max(guess, 14), 40);
return {
color: color,
fontSize: fontSize + 'px',
};
}
/*
* Return width or height depending on viz orientation
* */
get size() {
const { height, width } = this.props;
return this.isVertical ? height : width;
}
get isVertical() {
return this.props.orientation === VizOrientation.Vertical;
}
getBarGradient(maxSize: number): string {
const { minValue, maxValue, thresholds, value } = this.props;
const cssDirection = this.isVertical ? '0deg' : '90deg';
let gradient = '';
let lastpos = 0;
for (let i = 0; i < thresholds.length; i++) {
const threshold = thresholds[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
}
}
return gradient + ')';
}
renderBasicAndGradientBars(): ReactNode {
const { height, width, displayMode, maxValue, minValue, value } = this.props;
const { value } = this.props;
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
const maxSize = this.size * BAR_SIZE_RATIO;
const barSize = Math.max(valuePercent * maxSize, 0);
const colors = this.getValueColors();
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
const isBasic = displayMode === 'basic';
const containerStyles: CSSProperties = {
width: `${width}px`,
height: `${height}px`,
display: 'flex',
};
const barStyles: CSSProperties = {
borderRadius: '3px',
};
if (this.isVertical) {
// Custom styles for vertical orientation
containerStyles.flexDirection = 'column';
containerStyles.justifyContent = 'flex-end';
barStyles.transition = 'height 1s';
barStyles.height = `${barSize}px`;
barStyles.width = `${width}px`;
if (isBasic) {
// Basic styles
barStyles.background = `${colors.background}`;
barStyles.border = `1px solid ${colors.border}`;
barStyles.boxShadow = `0 0 4px ${colors.border}`;
} else {
// Gradient styles
barStyles.background = this.getBarGradient(maxSize);
}
} else {
// Custom styles for horizontal orientation
containerStyles.flexDirection = 'row-reverse';
containerStyles.justifyContent = 'flex-end';
containerStyles.alignItems = 'center';
barStyles.transition = 'width 1s';
barStyles.height = `${height}px`;
barStyles.width = `${barSize}px`;
barStyles.marginRight = '10px';
if (isBasic) {
// Basic styles
barStyles.background = `${colors.background}`;
barStyles.border = `1px solid ${colors.border}`;
barStyles.boxShadow = `0 0 4px ${colors.border}`;
} else {
// Gradient styles
barStyles.background = this.getBarGradient(maxSize);
}
}
const styles = getBasicAndGradientStyles(this.props);
return (
<div style={containerStyles}>
<div className="bar-gauge__value" style={valueStyles}>
<div style={styles.wrapper}>
<div className="bar-gauge__value" style={styles.value}>
{value.text}
</div>
<div style={barStyles} />
<div style={styles.bar} />
</div>
);
}
@ -197,7 +94,7 @@ export class BarGauge extends PureComponent<Props> {
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
return {
background: tinycolor(color)
.setAlpha(0.15)
.setAlpha(0.18)
.toRgbString(),
border: 'transparent',
isLit: false,
@ -205,7 +102,7 @@ export class BarGauge extends PureComponent<Props> {
} else {
return {
background: tinycolor(color)
.setAlpha(0.85)
.setAlpha(0.95)
.toRgbString(),
backgroundShade: tinycolor(color)
.setAlpha(0.55)
@ -225,31 +122,39 @@ export class BarGauge extends PureComponent<Props> {
}
renderRetroBars(): ReactNode {
const { height, width, maxValue, minValue, value } = this.props;
const { maxValue, minValue, value, itemSpacing } = this.props;
const {
valueHeight,
valueWidth,
maxBarHeight,
maxBarWidth,
wrapperWidth,
wrapperHeight,
} = calculateBarAndValueDimensions(this.props);
const isVert = isVertical(this.props);
const valueRange = maxValue - minValue;
const maxSize = this.size * BAR_SIZE_RATIO;
const cellSpacing = 5;
const maxSize = isVert ? maxBarHeight : maxBarWidth;
const cellSpacing = itemSpacing!;
const cellCount = maxSize / 20;
const cellSize = (maxSize - cellSpacing * cellCount) / cellCount;
const colors = this.getValueColors();
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
const valueColor = getValueColor(this.props);
const valueStyles = getValueStyles(value.text, valueColor, valueWidth, valueHeight);
const containerStyles: CSSProperties = {
width: `${width}px`,
height: `${height}px`,
width: `${wrapperWidth}px`,
height: `${wrapperHeight}px`,
display: 'flex',
};
if (this.isVertical) {
if (isVert) {
containerStyles.flexDirection = 'column-reverse';
containerStyles.alignItems = 'center';
valueStyles.marginBottom = '20px';
valueStyles.justifyContent = 'center';
} else {
containerStyles.flexDirection = 'row';
containerStyles.alignItems = 'center';
valueStyles.marginLeft = '20px';
valueStyles.justifyContent = 'flex-end';
}
const cells: JSX.Element[] = [];
@ -262,23 +167,22 @@ export class BarGauge extends PureComponent<Props> {
};
if (cellColor.isLit) {
cellStyles.boxShadow = `0 0 4px ${cellColor.border}`;
cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`;
} else {
cellStyles.backgroundColor = cellColor.background;
}
if (this.isVertical) {
if (isVert) {
cellStyles.height = `${cellSize}px`;
cellStyles.width = `${width}px`;
cellStyles.width = `${maxBarWidth}px`;
cellStyles.marginTop = `${cellSpacing}px`;
} else {
cellStyles.width = `${cellSize}px`;
cellStyles.height = `${height}px`;
cellStyles.height = `${maxBarHeight}px`;
cellStyles.marginRight = `${cellSpacing}px`;
}
cells.push(<div style={cellStyles} />);
cells.push(<div key={i.toString()} style={cellStyles} />);
}
return (
@ -292,15 +196,326 @@ export class BarGauge extends PureComponent<Props> {
}
}
interface BarColors {
value: string;
background: string;
border: string;
}
interface CellColors {
background: string;
backgroundShade?: string;
border: string;
isLit?: boolean;
}
interface TitleDimensions {
fontSize: number;
placement: 'above' | 'left' | 'below';
width: number;
height: number;
}
function isVertical(props: Props) {
return props.orientation === VizOrientation.Vertical;
}
function calculateTitleDimensions(props: Props): TitleDimensions {
const { title } = props.value;
const { height, width } = props;
if (!title) {
return { fontSize: 0, width: 0, height: 0, placement: 'above' };
}
if (isVertical(props)) {
return {
fontSize: 14,
width: width,
height: 14 * LINE_HEIGHT,
placement: 'below',
};
}
// if height above 40 put text to above bar
if (height > 40) {
const maxTitleHeightRatio = 0.35;
const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17);
return {
fontSize: titleHeight / LINE_HEIGHT,
width: 0,
height: titleHeight,
placement: 'above',
};
}
// title to left of bar scenario
const maxTitleHeightRatio = 0.6;
const maxTitleWidthRatio = 0.2;
const titleHeight = Math.max(height * maxTitleHeightRatio, MIN_VALUE_HEIGHT);
return {
fontSize: titleHeight / LINE_HEIGHT,
height: 0,
width: Math.min(Math.max(width * maxTitleWidthRatio, 50), 200),
placement: 'left',
};
}
export function getTitleStyles(props: Props): { wrapper: CSSProperties; title: CSSProperties } {
const wrapperStyles: CSSProperties = {
display: 'flex',
overflow: 'hidden',
};
const titleDim = calculateTitleDimensions(props);
const titleStyles: CSSProperties = {
fontSize: `${titleDim.fontSize}px`,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
alignItems: 'center',
alignSelf: 'center',
};
if (isVertical(props)) {
wrapperStyles.flexDirection = 'column-reverse';
titleStyles.textAlign = 'center';
} else {
if (titleDim.placement === 'above') {
wrapperStyles.flexDirection = 'column';
} else {
wrapperStyles.flexDirection = 'row';
titleStyles.width = `${titleDim.width}px`;
titleStyles.textAlign = 'right';
titleStyles.marginRight = '10px';
}
}
return {
wrapper: wrapperStyles,
title: titleStyles,
};
}
interface BasicAndGradientStyles {
wrapper: CSSProperties;
bar: CSSProperties;
value: CSSProperties;
}
interface BarAndValueDimensions {
valueWidth: number;
valueHeight: number;
maxBarWidth: number;
maxBarHeight: number;
wrapperHeight: number;
wrapperWidth: number;
}
function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
const { height, width } = props;
const titleDim = calculateTitleDimensions(props);
let maxBarHeight = 0;
let maxBarWidth = 0;
let valueHeight = 0;
let valueWidth = 0;
let wrapperWidth = 0;
let wrapperHeight = 0;
if (isVertical(props)) {
valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT);
valueWidth = width;
maxBarHeight = height - (titleDim.height + valueHeight);
maxBarWidth = width;
wrapperWidth = width;
wrapperHeight = height - titleDim.height;
} else {
valueHeight = height - titleDim.height;
valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), MIN_VALUE_WIDTH);
maxBarHeight = height - titleDim.height;
maxBarWidth = width - valueWidth - titleDim.width;
if (titleDim.placement === 'above') {
wrapperWidth = width;
wrapperHeight = height - titleDim.height;
} else {
wrapperWidth = width - titleDim.width;
wrapperHeight = height;
}
}
return {
valueWidth,
valueHeight,
maxBarWidth,
maxBarHeight,
wrapperHeight,
wrapperWidth,
};
}
/**
* Only exported to for unit test
*/
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
const { displayMode, maxValue, minValue, value } = props;
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
const valueColor = getValueColor(props);
const valueStyles = getValueStyles(value.text, valueColor, valueWidth, valueHeight);
const isBasic = displayMode === 'basic';
const wrapperStyles: CSSProperties = {
display: 'flex',
};
const barStyles: CSSProperties = {
borderRadius: '3px',
};
if (isVertical(props)) {
const barHeight = Math.max(valuePercent * maxBarHeight, 1);
// vertical styles
wrapperStyles.flexDirection = 'column';
wrapperStyles.justifyContent = 'flex-end';
barStyles.transition = 'height 1s';
barStyles.height = `${barHeight}px`;
barStyles.width = `${maxBarWidth}px`;
// value styles centered
valueStyles.justifyContent = 'center';
if (isBasic) {
// Basic styles
barStyles.background = `${tinycolor(valueColor)
.setAlpha(0.25)
.toRgbString()}`;
barStyles.borderTop = `2px solid ${valueColor}`;
} else {
// Gradient styles
barStyles.background = getBarGradient(props, maxBarHeight);
}
} else {
const barWidth = Math.max(valuePercent * maxBarWidth, 1);
// Custom styles for horizontal orientation
wrapperStyles.flexDirection = 'row-reverse';
wrapperStyles.justifyContent = 'flex-end';
wrapperStyles.alignItems = 'center';
barStyles.transition = 'width 1s';
barStyles.height = `${maxBarHeight}px`;
barStyles.width = `${barWidth}px`;
barStyles.marginRight = '10px';
if (isBasic) {
// Basic styles
barStyles.background = `${tinycolor(valueColor)
.setAlpha(0.25)
.toRgbString()}`;
barStyles.borderRight = `2px solid ${valueColor}`;
} else {
// Gradient styles
barStyles.background = getBarGradient(props, maxBarWidth);
}
}
return {
wrapper: wrapperStyles,
bar: barStyles,
value: valueStyles,
};
}
/**
* Only exported to for unit test
*/
export function getBarGradient(props: Props, maxSize: number): string {
const { minValue, maxValue, thresholds, value } = props;
const cssDirection = isVertical(props) ? '0deg' : '90deg';
let gradient = '';
let lastpos = 0;
for (let i = 0; i < thresholds.length; i++) {
const threshold = thresholds[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
}
}
return gradient + ')';
}
/**
* Only exported to for unit test
*/
export function getValueColor(props: Props): string {
const { thresholds, theme, value } = props;
const activeThreshold = getThresholdForValue(thresholds, value.numeric);
if (activeThreshold !== null) {
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
}
return getColorFromHexRgbOrName('gray', theme.type);
}
/**
* Only exported to for unit test
*/
function getValueStyles(value: string, color: string, width: number, height: number): CSSProperties {
const heightFont = height / LINE_HEIGHT;
const guess = width / (value.length * 1.1);
const fontSize = Math.min(Math.max(guess, 14), heightFont);
return {
color: color,
height: `${height}px`,
width: `${width}px`,
display: 'flex',
alignItems: 'center',
fontSize: fontSize.toFixed(2) + 'px',
};
}
// let canvasElement: HTMLCanvasElement | null = null;
//
// interface TextDimensions {
// width: number;
// height: number;
// }
//
// /**
// * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
// *
// * @param {String} text The text to be rendered.
// * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
// *
// * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
// */
// function getTextWidth(text: string): number {
// // re-use canvas object for better performance
// canvasElement = canvasElement || document.createElement('canvas');
// const context = canvasElement.getContext('2d');
// if (context) {
// context.font = 'normal 16px Roboto';
// const metrics = context.measureText(text);
// return metrics.width;
// }
// return 16;
// }

View File

@ -1,15 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render BarGauge with basic options should render 1`] = `
exports[`BarGauge Render with basic options should render 1`] = `
<div
style={
Object {
"alignItems": "center",
"display": "flex",
"flexDirection": "row-reverse",
"height": "300px",
"justifyContent": "flex-end",
"width": "300px",
}
}
>
@ -17,8 +15,12 @@ exports[`Render BarGauge with basic options should render 1`] = `
className="bar-gauge__value"
style={
Object {
"color": "#7EB26D",
"fontSize": "27.27272727272727px",
"alignItems": "center",
"color": "#73BF69",
"display": "flex",
"fontSize": "27.27px",
"height": "300px",
"width": "60px",
}
}
>
@ -27,10 +29,9 @@ exports[`Render BarGauge with basic options should render 1`] = `
<div
style={
Object {
"background": "rgba(126, 178, 109, 0.15)",
"border": "1px solid #7EB26D",
"background": "rgba(115, 191, 105, 0.25)",
"borderRadius": "3px",
"boxShadow": "0 0 4px #7EB26D",
"borderRight": "2px solid #73BF69",
"height": "300px",
"marginRight": "10px",
"transition": "width 1s",

View File

@ -31,7 +31,7 @@ export interface SingleStatValueOptions {
}
export interface GetSingleStatDisplayValueOptions {
data: SeriesData[];
data?: SeriesData[];
theme: GrafanaTheme;
valueMappings: ValueMapping[];
thresholds: Threshold[];
@ -55,24 +55,28 @@ export const getSingleStatDisplayValues = (options: GetSingleStatDisplayValueOpt
const values: DisplayValue[] = [];
for (const series of data) {
if (stat === 'name') {
values.push(display(series.name));
}
if (data) {
for (const series of data) {
if (stat === 'name') {
values.push(display(series.name));
}
for (let i = 0; i < series.fields.length; i++) {
const column = series.fields[i];
for (let i = 0; i < series.fields.length; i++) {
const column = series.fields[i];
// Show all fields that are not 'time'
if (column.type === FieldType.number) {
const stats = calculateStats({
series,
fieldIndex: i,
stats: [stat], // The stats to calculate
nullValueMode: NullValueMode.Null,
});
const displayValue = display(stats[stat]);
values.push(displayValue);
// Show all fields that are not 'time'
if (column.type === FieldType.number) {
const stats = calculateStats({
series,
fieldIndex: i,
stats: [stat], // The stats to calculate
nullValueMode: NullValueMode.Null,
});
const displayValue = display(stats[stat]);
displayValue.title = series.name;
values.push(displayValue);
}
}
}
}

View File

@ -1,23 +1,47 @@
import React, { PureComponent } from 'react';
import { VizOrientation } from '../../types';
interface RenderProps<T> {
vizWidth: number;
vizHeight: number;
value: T;
}
interface Props<T> {
children: (renderProps: RenderProps<T>) => JSX.Element | JSX.Element[];
renderValue: (value: T, width: number, height: number) => JSX.Element;
height: number;
width: number;
values: T[];
source: any; // If this changes, new values will be requested
getValues: () => T[];
renderCounter: number; // force update of values & render
orientation: VizOrientation;
itemSpacing?: number;
}
const SPACE_BETWEEN = 10;
interface DefaultProps {
itemSpacing: number;
}
type PropsWithDefaults<T> = Props<T> & DefaultProps;
interface State<T> {
values: T[];
}
export class VizRepeater<T> extends PureComponent<Props<T>, State<T>> {
static defaultProps: DefaultProps = {
itemSpacing: 10,
};
constructor(props: Props<T>) {
super(props);
this.state = {
values: props.getValues(),
};
}
componentDidUpdate(prevProps: Props<T>) {
const { renderCounter, source } = this.props;
if (renderCounter !== prevProps.renderCounter || source !== prevProps.source) {
this.setState({ values: this.props.getValues() });
}
}
export class VizRepeater<T> extends PureComponent<Props<T>> {
getOrientation(): VizOrientation {
const { orientation, width, height } = this.props;
@ -33,7 +57,8 @@ export class VizRepeater<T> extends PureComponent<Props<T>> {
}
render() {
const { children, height, values, width } = this.props;
const { renderValue, height, width, itemSpacing } = this.props as PropsWithDefaults<T>;
const { values } = this.state;
const orientation = this.getOrientation();
const itemStyles: React.CSSProperties = {
@ -49,14 +74,14 @@ export class VizRepeater<T> extends PureComponent<Props<T>> {
if (orientation === VizOrientation.Horizontal) {
repeaterStyle.flexDirection = 'column';
itemStyles.margin = `${SPACE_BETWEEN / 2}px 0`;
itemStyles.margin = `${itemSpacing / 2}px 0`;
vizWidth = width;
vizHeight = height / values.length - SPACE_BETWEEN;
vizHeight = height / values.length - itemSpacing;
} else {
repeaterStyle.flexDirection = 'row';
itemStyles.margin = `0 ${SPACE_BETWEEN / 2}px`;
itemStyles.margin = `0 ${itemSpacing / 2}px`;
vizHeight = height;
vizWidth = width / values.length - SPACE_BETWEEN;
vizWidth = width / values.length - itemSpacing;
}
itemStyles.width = `${vizWidth}px`;
@ -67,7 +92,7 @@ export class VizRepeater<T> extends PureComponent<Props<T>> {
{values.map((value, index) => {
return (
<div key={index} style={itemStyles}>
{children({ vizHeight, vizWidth, value })}
{renderValue(value, vizWidth, vizHeight)}
</div>
);
})}

View File

@ -2,12 +2,14 @@
import React, { PureComponent } from 'react';
// Services & Utils
import { DisplayValue, PanelProps, BarGauge, getSingleStatDisplayValues } from '@grafana/ui';
import { config } from 'app/core/config';
// Components
import { BarGauge, VizRepeater, getSingleStatDisplayValues } from '@grafana/ui/src/components';
// Types
import { BarGaugeOptions } from './types';
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
import { PanelProps, DisplayValue } from '@grafana/ui/src/types';
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
@ -21,12 +23,13 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
orientation={options.orientation}
thresholds={options.thresholds}
theme={config.theme}
itemSpacing={this.getItemSpacing()}
displayMode={options.displayMode}
/>
);
};
getProcessedValues = (): DisplayValue[] => {
getValues = (): DisplayValue[] => {
return getSingleStatDisplayValues({
valueMappings: this.props.options.valueMappings,
thresholds: this.props.options.thresholds,
@ -37,16 +40,25 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
});
};
getItemSpacing(): number {
if (this.props.options.displayMode === 'lcd') {
return 2;
}
return 10;
}
render() {
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
<VizRepeater
source={data}
getValues={this.getValues}
renderValue={this.renderValue}
renderCounter={renderCounter}
width={width}
height={height}
source={data}
renderCounter={renderCounter}
itemSpacing={this.getItemSpacing()}
orientation={options.orientation}
/>
);

View File

@ -9,8 +9,7 @@ import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
import { DisplayValue, PanelProps, getSingleStatDisplayValues } from '@grafana/ui';
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
import { DisplayValue, PanelProps, getSingleStatDisplayValues, VizRepeater } from '@grafana/ui';
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
@ -31,7 +30,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
);
};
getProcessedValues = (): DisplayValue[] => {
getValues = (): DisplayValue[] => {
return getSingleStatDisplayValues({
valueMappings: this.props.options.valueMappings,
thresholds: this.props.options.thresholds,
@ -45,8 +44,8 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
render() {
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
<VizRepeater
getValues={this.getValues}
renderValue={this.renderValue}
width={width}
height={height}

View File

@ -1,49 +0,0 @@
import React, { PureComponent } from 'react';
import { VizOrientation } from '@grafana/ui';
import { VizRepeater } from '@grafana/ui';
export interface Props<T> {
width: number;
height: number;
orientation: VizOrientation;
source: any; // If this changes, the values will be processed
renderCounter: number; // change to force processing
getProcessedValues: () => T[];
renderValue: (value: T, width: number, height: number) => JSX.Element;
}
interface State<T> {
values: T[];
}
/**
* This is essentially a cache of processed values. This checks for changes
* to the source and then saves the processed values in the State
*/
export class ProcessedValuesRepeater<T> extends PureComponent<Props<T>, State<T>> {
constructor(props: Props<T>) {
super(props);
this.state = {
values: props.getProcessedValues(),
};
}
componentDidUpdate(prevProps: Props<T>) {
const { renderCounter, source } = this.props;
if (renderCounter !== prevProps.renderCounter || source !== prevProps.source) {
this.setState({ values: this.props.getProcessedValues() });
}
}
render() {
const { orientation, height, width, renderValue } = this.props;
const { values } = this.state;
return (
<VizRepeater height={height} width={width} values={values} orientation={orientation}>
{({ vizHeight, vizWidth, value }) => renderValue(value, vizWidth, vizHeight)}
</VizRepeater>
);
}
}

View File

@ -6,11 +6,11 @@ import { config } from 'app/core/config';
import { getFlotPairs } from '@grafana/ui/src/utils/flotPairs';
// Components
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
import { VizRepeater } from '@grafana/ui/src/components';
import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
// Types
import { SingleStatOptions } from './types';
import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
import {
DisplayValue,
PanelProps,
@ -34,7 +34,7 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
return <BigValue {...value} width={width} height={height} theme={config.theme} />;
};
getProcessedValues = (): SingleStatDisplay[] => {
getValues = (): SingleStatDisplay[] => {
const { data, replaceVariables, options, timeRange } = this.props;
const { valueOptions, valueMappings } = options;
@ -127,8 +127,8 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
render() {
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
<VizRepeater
getValues={this.getValues}
renderValue={this.renderValue}
width={width}
height={height}