diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx index c7a53af5ccf..6754d43a5cc 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx @@ -1,6 +1,7 @@ import { storiesOf } from '@storybook/react'; -import { number, text } from '@storybook/addon-knobs'; +import { number, text, boolean } from '@storybook/addon-knobs'; import { BarGauge } from './BarGauge'; +import { VizOrientation } from '../../types'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; @@ -15,6 +16,8 @@ const getKnobs = () => { threshold2Color: text('threshold2Color', 'red'), unit: text('unit', 'ms'), decimals: number('decimals', 1), + horizontal: boolean('horizontal', false), + lcd: boolean('lcd', false), }; }; @@ -22,7 +25,7 @@ const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module); BarGaugeStories.addDecorator(withCenteredStory); -BarGaugeStories.add('Vertical, with basic thresholds', () => { +BarGaugeStories.add('Simple with basic thresholds', () => { const { value, minValue, @@ -33,11 +36,13 @@ BarGaugeStories.add('Vertical, with basic thresholds', () => { threshold2Value, unit, decimals, + horizontal, + lcd, } = getKnobs(); return renderComponentWithTheme(BarGauge, { - width: 200, - height: 400, + width: 700, + height: 700, value: value, minValue: minValue, maxValue: maxValue, @@ -45,6 +50,8 @@ BarGaugeStories.add('Vertical, with basic thresholds', () => { 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 }, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index 9d592ef310b..69cdbf5bff1 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => { const props: Props = { maxValue: 100, minValue: 0, + displayMode: 'basic', thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], height: 300, width: 300, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index aadd4dbcf3b..99d2d061a11 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -1,5 +1,5 @@ // Library -import React, { PureComponent, CSSProperties } from 'react'; +import React, { PureComponent, CSSProperties, ReactNode } from 'react'; import tinycolor from 'tinycolor2'; // Utils @@ -18,11 +18,9 @@ export interface Props extends Themeable { maxValue: number; minValue: number; orientation: VizOrientation; + displayMode: 'basic' | 'lcd' | 'gradient'; } -/* - * This visualization is still in POC state, needed more tests & better structure - */ export class BarGauge extends PureComponent { static defaultProps: Partial = { maxValue: 100, @@ -31,10 +29,22 @@ export class BarGauge extends PureComponent { text: '100', numeric: 100, }, + displayMode: 'lcd', orientation: VizOrientation.Horizontal, thresholds: [], }; + render() { + switch (this.props.displayMode) { + case 'lcd': + return this.renderRetroBars(); + case 'basic': + case 'gradient': + default: + return this.renderBasicAndGradientBars(); + } + } + getValueColors(): BarColors { const { thresholds, theme, value } = this.props; @@ -46,41 +56,19 @@ export class BarGauge extends PureComponent { return { value: color, border: color, - bar: tinycolor(color) - .setAlpha(0.3) + background: tinycolor(color) + .setAlpha(0.15) .toRgbString(), }; } return { value: getColorFromHexRgbOrName('gray', theme.type), - bar: getColorFromHexRgbOrName('gray', theme.type), + background: getColorFromHexRgbOrName('gray', theme.type), border: getColorFromHexRgbOrName('gray', theme.type), }; } - getCellColor(positionValue: TimeSeriesValue): string { - const { thresholds, theme, value } = this.props; - const activeThreshold = getThresholdForValue(thresholds, positionValue); - - if (activeThreshold !== null) { - const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type); - - // if we are past real value the cell is not "on" - if (value === null || (positionValue !== null && positionValue > value.numeric)) { - return tinycolor(color) - .setAlpha(0.15) - .toRgbString(); - } else { - return tinycolor(color) - .setAlpha(0.7) - .toRgbString(); - } - } - - return 'gray'; - } - getValueStyles(value: string, color: string, width: number): CSSProperties { const guess = width / (value.length * 1.1); const fontSize = Math.min(Math.max(guess, 14), 40); @@ -91,107 +79,205 @@ export class BarGauge extends PureComponent { }; } - renderVerticalBar(valueFormatted: string, valuePercent: number) { + /* + * Return width or height depending on viz orientation + * */ + get size() { const { height, width } = this.props; + return this.isVertical ? height : width; + } - const maxHeight = height * BAR_SIZE_RATIO; - const barHeight = Math.max(valuePercent * maxHeight, 0); + 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 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 valueStyles = this.getValueStyles(valueFormatted, colors.value, width); + 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', - flexDirection: 'column', - justifyContent: 'flex-end', }; const barStyles: CSSProperties = { - height: `${barHeight}px`, - width: `${width}px`, - backgroundColor: colors.bar, - borderTop: `1px solid ${colors.border}`, + 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); + } + } + return (
- {valueFormatted} + {value.text}
); } - renderHorizontalBar(valueFormatted: string, valuePercent: number) { - const { height, width } = this.props; + getCellColor(positionValue: TimeSeriesValue): CellColors { + const { thresholds, theme, value } = this.props; + const activeThreshold = getThresholdForValue(thresholds, positionValue); - const maxWidth = width * BAR_SIZE_RATIO; - const barWidth = Math.max(valuePercent * maxWidth, 0); - const colors = this.getValueColors(); - const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO)); + if (activeThreshold !== null) { + const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type); - valueStyles.marginLeft = '8px'; + // if we are past real value the cell is not "on" + if (value === null || (positionValue !== null && positionValue > value.numeric)) { + return { + background: tinycolor(color) + .setAlpha(0.15) + .toRgbString(), + border: 'transparent', + isLit: false, + }; + } else { + return { + background: tinycolor(color) + .setAlpha(0.85) + .toRgbString(), + backgroundShade: tinycolor(color) + .setAlpha(0.55) + .toRgbString(), + border: tinycolor(color) + .setAlpha(0.9) + .toRgbString(), + isLit: true, + }; + } + } - const containerStyles: CSSProperties = { - width: `${width}px`, - height: `${height}px`, - display: 'flex', - flexDirection: 'row', - alignItems: 'center', + return { + background: 'gray', + border: 'gray', }; - - const barStyles = { - height: `${height}px`, - width: `${barWidth}px`, - backgroundColor: colors.bar, - borderRight: `1px solid ${colors.border}`, - }; - - return ( -
-
-
- {valueFormatted} -
-
- ); } - renderHorizontalLCD(valueFormatted: string, valuePercent: number) { - const { height, width, maxValue, minValue } = this.props; + renderRetroBars(): ReactNode { + const { height, width, maxValue, minValue, value } = this.props; const valueRange = maxValue - minValue; - const maxWidth = width * BAR_SIZE_RATIO; - const cellSpacing = 4; - const cellCount = 30; - const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount; + const maxSize = this.size * BAR_SIZE_RATIO; + const cellSpacing = 5; + const cellCount = maxSize / 20; + const cellSize = (maxSize - cellSpacing * cellCount) / cellCount; const colors = this.getValueColors(); - const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO)); - valueStyles.marginLeft = '8px'; + const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height); + const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText); const containerStyles: CSSProperties = { width: `${width}px`, height: `${height}px`, display: 'flex', - flexDirection: 'row', - alignItems: 'center', }; + if (this.isVertical) { + containerStyles.flexDirection = 'column-reverse'; + containerStyles.alignItems = 'center'; + valueStyles.marginBottom = '20px'; + } else { + containerStyles.flexDirection = 'row'; + containerStyles.alignItems = 'center'; + valueStyles.marginLeft = '20px'; + } + const cells: JSX.Element[] = []; for (let i = 0; i < cellCount; i++) { const currentValue = (valueRange / cellCount) * i; const cellColor = this.getCellColor(currentValue); const cellStyles: CSSProperties = { - width: `${cellWidth}px`, - backgroundColor: cellColor, - marginRight: '4px', - height: `${height}px`, borderRadius: '2px', }; + 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) { + cellStyles.height = `${cellSize}px`; + cellStyles.width = `${width}px`; + cellStyles.marginTop = `${cellSpacing}px`; + } else { + cellStyles.width = `${cellSize}px`; + cellStyles.height = `${height}px`; + cellStyles.marginRight = `${cellSpacing}px`; + } + cells.push(
); } @@ -199,26 +285,22 @@ export class BarGauge extends PureComponent {
{cells}
- {valueFormatted} + {value.text}
); } - - render() { - const { maxValue, minValue, orientation, value } = this.props; - - const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1); - const vertical = orientation === 'vertical'; - - return vertical - ? this.renderVerticalBar(value.text, valuePercent) - : this.renderHorizontalLCD(value.text, valuePercent); - } } interface BarColors { value: string; - bar: string; + background: string; border: string; } + +interface CellColors { + background: string; + backgroundShade?: string; + border: string; + isLit?: boolean; +} diff --git a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap index 65c647bd90c..25a2e79b647 100644 --- a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap +++ b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap @@ -6,353 +6,37 @@ exports[`Render BarGauge with basic options should render 1`] = ` Object { "alignItems": "center", "display": "flex", - "flexDirection": "row", + "flexDirection": "row-reverse", "height": "300px", + "justifyContent": "flex-end", "width": "300px", } } > -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
25
+
`; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index adacf393a09..daecff37dc8 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -166,7 +166,11 @@ export class ThresholdsEditor extends PureComponent {
{threshold.color && (
- this.onChangeThresholdColor(threshold, color)} /> + this.onChangeThresholdColor(threshold, color)} + enableNamedColors={true} + />
)}
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap index bd0ab03bf51..2bc5ee56d4d 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -83,10 +83,12 @@ exports[`Render should render with base threshold 1`] = ` > > { orientation={options.orientation} thresholds={options.thresholds} theme={config.theme} + displayMode={options.displayMode} /> ); }; diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx index cccd4e88b8e..3404c8d8805 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx @@ -6,7 +6,7 @@ import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGr // Types import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui'; -import { BarGaugeOptions, orientationOptions } from './types'; +import { BarGaugeOptions, orientationOptions, displayModes } from './types'; import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor'; import { SingleStatValueOptions } from '../singlestat2/types'; @@ -32,6 +32,7 @@ export class BarGaugePanelEditor extends PureComponent this.props.onOptionsChange({ ...this.props.options, minValue: target.value }); onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value }); onOrientationChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, orientation: value }); + onDisplayModeChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, displayMode: value }); render() { const { options } = this.props; @@ -53,6 +54,16 @@ export class BarGaugePanelEditor extends PureComponent item.value === options.orientation)} />
+
+ Display Mode +