mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14934 from grafana/hugoh/refactor-gauge-to-work-with-thresholds
Refactor gauge to work with thresholds
This commit is contained in:
commit
a0eddad323
1250
devenv/dev-dashboards/panel_tests_gauge.json
Normal file
1250
devenv/dev-dashboards/panel_tests_gauge.json
Normal file
File diff suppressed because it is too large
Load Diff
224
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
Normal file
224
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { Gauge, Props } from './Gauge';
|
||||||
|
import { TimeSeriesVMs } from '../../types/series';
|
||||||
|
import { ValueMapping, MappingType } from '../../types';
|
||||||
|
|
||||||
|
jest.mock('jquery', () => ({
|
||||||
|
plot: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
maxValue: 100,
|
||||||
|
valueMappings: [],
|
||||||
|
minValue: 0,
|
||||||
|
prefix: '',
|
||||||
|
showThresholdMarkers: true,
|
||||||
|
showThresholdLabels: false,
|
||||||
|
suffix: '',
|
||||||
|
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||||
|
unit: 'none',
|
||||||
|
stat: 'avg',
|
||||||
|
height: 300,
|
||||||
|
width: 300,
|
||||||
|
timeSeries: {} as TimeSeriesVMs,
|
||||||
|
decimals: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<Gauge {...props} />);
|
||||||
|
const instance = wrapper.instance() as Gauge;
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
wrapper,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Get font color', () => {
|
||||||
|
it('should get first threshold color when only one threshold', () => {
|
||||||
|
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||||
|
|
||||||
|
expect(instance.getFontColor(49)).toEqual('#7EB26D');
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instance.getFontColor(50)).toEqual('#EAB839');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the nearest threshold color between thresholds', () => {
|
||||||
|
const { instance } = setup({
|
||||||
|
thresholds: [
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instance.getFontColor(55)).toEqual('#EAB839');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Get thresholds formatted', () => {
|
||||||
|
it('should return first thresholds color for min and max', () => {
|
||||||
|
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||||
|
|
||||||
|
expect(instance.getFormattedThresholds()).toEqual([
|
||||||
|
{ value: 0, color: '#7EB26D' },
|
||||||
|
{ value: 100, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the correct formatted values when thresholds are added', () => {
|
||||||
|
const { instance } = setup({
|
||||||
|
thresholds: [
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instance.getFormattedThresholds()).toEqual([
|
||||||
|
{ value: 0, color: '#7EB26D' },
|
||||||
|
{ value: 50, color: '#7EB26D' },
|
||||||
|
{ value: 75, color: '#EAB839' },
|
||||||
|
{ value: 100, color: '#6ED0E0' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Format value with value mappings', () => {
|
||||||
|
it('should return undefined with no valuemappings', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined with no matching valuemappings', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
|
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
||||||
|
];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first matching mapping with lowest id', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
|
||||||
|
{ id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
|
||||||
|
];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
|
||||||
|
expect(result.text).toEqual('1-20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rangeToText mapping where value equals to', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
|
||||||
|
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
|
];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
|
||||||
|
expect(result.text).toEqual('1-10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rangeToText mapping where value equals from', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
|
||||||
|
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
|
];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
|
||||||
|
expect(result.text).toEqual('10-20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rangeToText mapping where value is between from and to', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
|
||||||
|
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
|
];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
|
||||||
|
expect(result.text).toEqual('1-20');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Format value', () => {
|
||||||
|
it('should return if value isNaN', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [];
|
||||||
|
const value = 'N/A';
|
||||||
|
const { instance } = setup({ valueMappings });
|
||||||
|
|
||||||
|
const result = instance.formatValue(value);
|
||||||
|
|
||||||
|
expect(result).toEqual('N/A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return formatted value if there are no value mappings', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [];
|
||||||
|
const value = '6';
|
||||||
|
const { instance } = setup({ valueMappings, decimals: 1 });
|
||||||
|
|
||||||
|
const result = instance.formatValue(value);
|
||||||
|
|
||||||
|
expect(result).toEqual(' 6.0 ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return formatted value if there are no matching value mappings', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
|
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
||||||
|
];
|
||||||
|
const value = '10';
|
||||||
|
const { instance } = setup({ valueMappings, decimals: 1 });
|
||||||
|
|
||||||
|
const result = instance.formatValue(value);
|
||||||
|
|
||||||
|
expect(result).toEqual(' 10.0 ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return mapped value if there are matching value mappings', () => {
|
||||||
|
const valueMappings: ValueMapping[] = [
|
||||||
|
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
|
||||||
|
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||||
|
];
|
||||||
|
const value = '11';
|
||||||
|
const { instance } = setup({ valueMappings, decimals: 1 });
|
||||||
|
|
||||||
|
const result = instance.formatValue(value);
|
||||||
|
|
||||||
|
expect(result).toEqual(' 1-20 ');
|
||||||
|
});
|
||||||
|
});
|
284
packages/grafana-ui/src/components/Gauge/Gauge.tsx
Normal file
284
packages/grafana-ui/src/components/Gauge/Gauge.tsx
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ValueMapping,
|
||||||
|
Threshold,
|
||||||
|
ThemeName,
|
||||||
|
MappingType,
|
||||||
|
BasicGaugeColor,
|
||||||
|
ThemeNames,
|
||||||
|
ValueMap,
|
||||||
|
RangeMap,
|
||||||
|
} from '../../types/panel';
|
||||||
|
import { TimeSeriesVMs } from '../../types/series';
|
||||||
|
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
||||||
|
|
||||||
|
type TimeSeriesValue = string | number | null;
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
decimals: number;
|
||||||
|
height: number;
|
||||||
|
valueMappings: ValueMapping[];
|
||||||
|
maxValue: number;
|
||||||
|
minValue: number;
|
||||||
|
prefix: string;
|
||||||
|
timeSeries: TimeSeriesVMs;
|
||||||
|
thresholds: Threshold[];
|
||||||
|
showThresholdMarkers: boolean;
|
||||||
|
showThresholdLabels: boolean;
|
||||||
|
stat: string;
|
||||||
|
suffix: string;
|
||||||
|
unit: string;
|
||||||
|
width: number;
|
||||||
|
theme?: ThemeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Gauge extends PureComponent<Props> {
|
||||||
|
canvasElement: any;
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
maxValue: 100,
|
||||||
|
valueMappings: [],
|
||||||
|
minValue: 0,
|
||||||
|
prefix: '',
|
||||||
|
showThresholdMarkers: true,
|
||||||
|
showThresholdLabels: false,
|
||||||
|
suffix: '',
|
||||||
|
thresholds: [],
|
||||||
|
unit: 'none',
|
||||||
|
stat: 'avg',
|
||||||
|
theme: ThemeNames.Dark,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) {
|
||||||
|
if (!valueToTextMapping.value) {
|
||||||
|
return allValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueAsNumber = parseFloat(value as string);
|
||||||
|
const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
|
||||||
|
|
||||||
|
if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
|
||||||
|
return allValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAsNumber !== valueToTextMappingAsNumber) {
|
||||||
|
return allValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allValueMappings.concat(valueToTextMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) {
|
||||||
|
if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) {
|
||||||
|
return allValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueAsNumber = parseFloat(value as string);
|
||||||
|
const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
|
||||||
|
const toAsNumber = parseFloat(rangeToTextMapping.to as string);
|
||||||
|
|
||||||
|
if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
|
||||||
|
return allValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
|
||||||
|
return allValueMappings.concat(rangeToTextMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) {
|
||||||
|
const allFormattedValueMappings = valueMappings.reduce(
|
||||||
|
(allValueMappings, valueMapping) => {
|
||||||
|
if (valueMapping.type === MappingType.ValueToText) {
|
||||||
|
allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
|
||||||
|
} else if (valueMapping.type === MappingType.RangeToText) {
|
||||||
|
allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allValueMappings;
|
||||||
|
},
|
||||||
|
[] as ValueMapping[]
|
||||||
|
);
|
||||||
|
|
||||||
|
allFormattedValueMappings.sort((t1, t2) => {
|
||||||
|
return t1.id - t2.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return allFormattedValueMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) {
|
||||||
|
return this.getAllFormattedValueMappings(valueMappings, value)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
formatValue(value: TimeSeriesValue) {
|
||||||
|
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||||
|
|
||||||
|
if (isNaN(value as number)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueMappings.length > 0) {
|
||||||
|
const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value);
|
||||||
|
if (valueMappedValue) {
|
||||||
|
return `${prefix} ${valueMappedValue.text} ${suffix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFunc = getValueFormat(unit);
|
||||||
|
const formattedValue = formatFunc(value as number, decimals);
|
||||||
|
|
||||||
|
return `${prefix} ${formattedValue} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFontColor(value: TimeSeriesValue) {
|
||||||
|
const { thresholds } = this.props;
|
||||||
|
|
||||||
|
if (thresholds.length === 1) {
|
||||||
|
return thresholds[0].color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||||
|
if (atThreshold) {
|
||||||
|
return atThreshold.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
|
||||||
|
|
||||||
|
if (belowThreshold.length > 0) {
|
||||||
|
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
|
||||||
|
return nearestThreshold.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BasicGaugeColor.Red;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormattedThresholds() {
|
||||||
|
const { maxValue, minValue, thresholds } = this.props;
|
||||||
|
|
||||||
|
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
|
||||||
|
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
|
||||||
|
|
||||||
|
const formattedThresholds = [
|
||||||
|
...thresholdsSortedByIndex.map(threshold => {
|
||||||
|
if (threshold.index === 0) {
|
||||||
|
return { value: minValue, color: threshold.color };
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
|
||||||
|
return { value: threshold.value, color: previousThreshold.color };
|
||||||
|
}),
|
||||||
|
{ value: maxValue, color: lastThreshold.color },
|
||||||
|
];
|
||||||
|
|
||||||
|
return formattedThresholds;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const {
|
||||||
|
maxValue,
|
||||||
|
minValue,
|
||||||
|
timeSeries,
|
||||||
|
showThresholdLabels,
|
||||||
|
showThresholdMarkers,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
stat,
|
||||||
|
theme,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let value: TimeSeriesValue = '';
|
||||||
|
|
||||||
|
if (timeSeries[0]) {
|
||||||
|
value = timeSeries[0].stats[stat];
|
||||||
|
} else {
|
||||||
|
value = 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimension = Math.min(width, height * 1.3);
|
||||||
|
const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||||
|
const fontScale = parseInt('80', 10) / 100;
|
||||||
|
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||||
|
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||||
|
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||||
|
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||||
|
const thresholdLabelFontSize = fontSize / 2.5;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
series: {
|
||||||
|
gauges: {
|
||||||
|
gauge: {
|
||||||
|
min: minValue,
|
||||||
|
max: maxValue,
|
||||||
|
background: { color: backgroundColor },
|
||||||
|
border: { color: null },
|
||||||
|
shadow: { show: false },
|
||||||
|
width: gaugeWidth,
|
||||||
|
},
|
||||||
|
frame: { show: false },
|
||||||
|
label: { show: false },
|
||||||
|
layout: { margin: 0, thresholdWidth: 0 },
|
||||||
|
cell: { border: { width: 0 } },
|
||||||
|
threshold: {
|
||||||
|
values: this.getFormattedThresholds(),
|
||||||
|
label: {
|
||||||
|
show: showThresholdLabels,
|
||||||
|
margin: thresholdMarkersWidth + 1,
|
||||||
|
font: { size: thresholdLabelFontSize },
|
||||||
|
},
|
||||||
|
show: showThresholdMarkers,
|
||||||
|
width: thresholdMarkersWidth,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
color: this.getFontColor(value),
|
||||||
|
formatter: () => {
|
||||||
|
return this.formatValue(value);
|
||||||
|
},
|
||||||
|
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||||
|
},
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const plotSeries = { data: [[0, value]] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
$.plot(this.canvasElement, [plotSeries], options);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Gauge rendering error', err, options, timeSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { height, width } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="singlestat-panel">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${height * 0.9}px`,
|
||||||
|
width: `${Math.min(width, height * 1.3)}px`,
|
||||||
|
top: '10px',
|
||||||
|
margin: 'auto',
|
||||||
|
}}
|
||||||
|
ref={element => (this.canvasElement = element)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gauge;
|
@ -19,9 +19,15 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const thresholds: Threshold[] =
|
const addDefaultThreshold = this.props.thresholds.length === 0;
|
||||||
props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
|
const thresholds: Threshold[] = addDefaultThreshold
|
||||||
|
? [{ index: 0, value: -Infinity, color: colors[0] }]
|
||||||
|
: props.thresholds;
|
||||||
this.state = { thresholds };
|
this.state = { thresholds };
|
||||||
|
|
||||||
|
if (addDefaultThreshold) {
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddThreshold = (index: number) => {
|
onAddThreshold = (index: number) => {
|
||||||
@ -62,7 +68,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
() => this.updateGauge()
|
() => this.onChange()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,7 +91,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
thresholds: newThresholds.filter(t => t !== threshold),
|
thresholds: newThresholds.filter(t => t !== threshold),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
() => this.updateGauge()
|
() => this.onChange()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,7 +105,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
const value = isNaN(parsedValue) ? null : parsedValue;
|
const value = isNaN(parsedValue) ? null : parsedValue;
|
||||||
|
|
||||||
const newThresholds = thresholds.map(t => {
|
const newThresholds = thresholds.map(t => {
|
||||||
if (t === threshold) {
|
if (t === threshold && t.index !== 0) {
|
||||||
t = { ...t, value: value as number };
|
t = { ...t, value: value as number };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
{
|
{
|
||||||
thresholds: newThresholds,
|
thresholds: newThresholds,
|
||||||
},
|
},
|
||||||
() => this.updateGauge()
|
() => this.onChange()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||||
@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
return { thresholds: sortThresholds };
|
return { thresholds: sortThresholds };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateGauge();
|
this.onChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateGauge = () => {
|
onChange = () => {
|
||||||
this.props.onChange(this.state.thresholds);
|
this.props.onChange(this.state.thresholds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,3 +22,4 @@ export { Graph } from './Graph/Graph';
|
|||||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||||
|
export { Gauge } from './Gauge/Gauge';
|
||||||
|
@ -66,3 +66,10 @@ export interface RangeMap extends BaseMap {
|
|||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThemeName = 'dark' | 'light';
|
||||||
|
|
||||||
|
export enum ThemeNames {
|
||||||
|
Dark = 'dark',
|
||||||
|
Light = 'light',
|
||||||
|
}
|
||||||
|
@ -21,9 +21,12 @@ export interface TimeSeriesVM {
|
|||||||
color: string;
|
color: string;
|
||||||
data: TimeSeriesValue[][];
|
data: TimeSeriesValue[][];
|
||||||
stats: TimeSeriesStats;
|
stats: TimeSeriesStats;
|
||||||
|
allIsNull: boolean;
|
||||||
|
allIsZero: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeSeriesStats {
|
export interface TimeSeriesStats {
|
||||||
|
[key: string]: number | null;
|
||||||
total: number | null;
|
total: number | null;
|
||||||
max: number | null;
|
max: number | null;
|
||||||
min: number | null;
|
min: number | null;
|
||||||
@ -36,8 +39,6 @@ export interface TimeSeriesStats {
|
|||||||
range: number | null;
|
range: number | null;
|
||||||
timeStep: number;
|
timeStep: number;
|
||||||
count: number;
|
count: number;
|
||||||
allIsNull: boolean;
|
|
||||||
allIsZero: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NullValueMode {
|
export enum NullValueMode {
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { colors } from './colors';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
timeSeries: TimeSeries[];
|
timeSeries: TimeSeries[];
|
||||||
nullValueMode: NullValueMode;
|
nullValueMode: NullValueMode;
|
||||||
colorPalette: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs {
|
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
|
||||||
const vmSeries = timeSeries.map((item, index) => {
|
const vmSeries = timeSeries.map((item, index) => {
|
||||||
const colorIndex = index % colorPalette.length;
|
const colorIndex = index % colors.length;
|
||||||
const label = item.target;
|
const label = item.target;
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
@ -49,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof currentValue !== 'number') {
|
if (currentValue !== null && typeof currentValue !== 'number') {
|
||||||
continue;
|
throw {message: 'Time series contains non number values'};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due to missing values we could have different timeStep all along the series
|
// Due to missing values we could have different timeStep all along the series
|
||||||
@ -150,7 +151,9 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
|||||||
return {
|
return {
|
||||||
data: result,
|
data: result,
|
||||||
label: label,
|
label: label,
|
||||||
color: colorPalette[colorIndex],
|
color: colors[colorIndex],
|
||||||
|
allIsZero,
|
||||||
|
allIsNull,
|
||||||
stats: {
|
stats: {
|
||||||
total,
|
total,
|
||||||
min,
|
min,
|
||||||
@ -164,8 +167,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
|||||||
range,
|
range,
|
||||||
count,
|
count,
|
||||||
first,
|
first,
|
||||||
allIsZero,
|
|
||||||
allIsNull,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import config from 'app/core/config';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
|
import { ThemeNames, ThemeName } from '@grafana/ui';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
isGrafanaAdmin: any;
|
isGrafanaAdmin: any;
|
||||||
@ -59,6 +60,10 @@ export class ContextSrv {
|
|||||||
this.sidemenu = !this.sidemenu;
|
this.sidemenu = !this.sidemenu;
|
||||||
store.set('grafana.sidemenu', this.sidemenu);
|
store.set('grafana.sidemenu', this.sidemenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTheme(): ThemeName {
|
||||||
|
return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextSrv = new ContextSrv();
|
const contextSrv = new ContextSrv();
|
||||||
|
@ -55,7 +55,6 @@ const mustKeepProps: { [str: string]: boolean } = {
|
|||||||
hasRefreshed: true,
|
hasRefreshed: true,
|
||||||
events: true,
|
events: true,
|
||||||
cacheTimeout: true,
|
cacheTimeout: true,
|
||||||
nullPointMode: true,
|
|
||||||
cachedPluginOptions: true,
|
cachedPluginOptions: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
};
|
};
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { PanelProps, NullValueMode } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
// Services & Utils
|
||||||
import Gauge from 'app/viz/Gauge';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { processTimeSeries } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Gauge } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Types
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
|
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
|
||||||
|
|
||||||
interface Props extends PanelProps<GaugeOptions> {}
|
interface Props extends PanelProps<GaugeOptions> {}
|
||||||
|
|
||||||
export class GaugePanel extends PureComponent<Props> {
|
export class GaugePanel extends PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
||||||
|
|
||||||
const prefix = onInterpolate(options.prefix);
|
const prefix = onInterpolate(options.prefix);
|
||||||
const suffix = onInterpolate(options.suffix);
|
const suffix = onInterpolate(options.suffix);
|
||||||
|
|
||||||
const vmSeries = getTimeSeriesVMs({
|
const vmSeries = processTimeSeries({
|
||||||
timeSeries: timeSeries,
|
timeSeries: timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -27,6 +35,7 @@ export class GaugePanel extends PureComponent<Props> {
|
|||||||
height={height}
|
height={height}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
suffix={suffix}
|
suffix={suffix}
|
||||||
|
theme={contextSrv.getTheme()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import {
|
import {
|
||||||
BasicGaugeColor,
|
|
||||||
PanelOptionsProps,
|
PanelOptionsProps,
|
||||||
ThresholdsEditor,
|
ThresholdsEditor,
|
||||||
Threshold,
|
Threshold,
|
||||||
@ -15,7 +14,6 @@ import { GaugeOptions } from './types';
|
|||||||
|
|
||||||
export const defaultProps = {
|
export const defaultProps = {
|
||||||
options: {
|
options: {
|
||||||
baseColor: BasicGaugeColor.Green,
|
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||||
|
|
||||||
export interface GaugeOptions {
|
export interface GaugeOptions {
|
||||||
baseColor: string;
|
|
||||||
decimals: number;
|
decimals: number;
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { colors } from '@grafana/ui';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { processTimeSeries } from '@grafana/ui/src/utils';
|
import { processTimeSeries } from '@grafana/ui/src/utils';
|
||||||
@ -23,7 +22,6 @@ export class GraphPanel extends PureComponent<Props> {
|
|||||||
const vmSeries = processTimeSeries({
|
const vmSeries = processTimeSeries({
|
||||||
timeSeries: timeSeries,
|
timeSeries: timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Ignore,
|
||||||
colorPalette: colors,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { Gauge, Props } from './Gauge';
|
|
||||||
|
|
||||||
jest.mock('jquery', () => ({
|
|
||||||
plot: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
|
||||||
const props: Props = {
|
|
||||||
baseColor: BasicGaugeColor.Green,
|
|
||||||
maxValue: 100,
|
|
||||||
valueMappings: [],
|
|
||||||
minValue: 0,
|
|
||||||
prefix: '',
|
|
||||||
showThresholdMarkers: true,
|
|
||||||
showThresholdLabels: false,
|
|
||||||
suffix: '',
|
|
||||||
thresholds: [],
|
|
||||||
unit: 'none',
|
|
||||||
stat: 'avg',
|
|
||||||
height: 300,
|
|
||||||
width: 300,
|
|
||||||
timeSeries: {} as TimeSeriesVMs,
|
|
||||||
decimals: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
|
||||||
|
|
||||||
const wrapper = shallow(<Gauge {...props} />);
|
|
||||||
const instance = wrapper.instance() as Gauge;
|
|
||||||
|
|
||||||
return {
|
|
||||||
instance,
|
|
||||||
wrapper,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Get font color', () => {
|
|
||||||
it('should get base color if no threshold', () => {
|
|
||||||
const { instance } = setup();
|
|
||||||
|
|
||||||
expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be f2f2f2', () => {
|
|
||||||
const { instance } = setup({
|
|
||||||
thresholds: [{ value: 59, color: '#f2f2f2' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(instance.getFontColor(58)).toEqual('#f2f2f2');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,216 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import $ from 'jquery';
|
|
||||||
import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
|
|
||||||
|
|
||||||
import config from '../core/config';
|
|
||||||
import kbn from '../core/utils/kbn';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
baseColor: string;
|
|
||||||
decimals: number;
|
|
||||||
height: number;
|
|
||||||
valueMappings: ValueMapping[];
|
|
||||||
maxValue: number;
|
|
||||||
minValue: number;
|
|
||||||
prefix: string;
|
|
||||||
timeSeries: TimeSeriesVMs;
|
|
||||||
thresholds: Threshold[];
|
|
||||||
showThresholdMarkers: boolean;
|
|
||||||
showThresholdLabels: boolean;
|
|
||||||
stat: string;
|
|
||||||
suffix: string;
|
|
||||||
unit: string;
|
|
||||||
width: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Gauge extends PureComponent<Props> {
|
|
||||||
canvasElement: any;
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
baseColor: BasicGaugeColor.Green,
|
|
||||||
maxValue: 100,
|
|
||||||
valueMappings: [],
|
|
||||||
minValue: 0,
|
|
||||||
prefix: '',
|
|
||||||
showThresholdMarkers: true,
|
|
||||||
showThresholdLabels: false,
|
|
||||||
suffix: '',
|
|
||||||
thresholds: [],
|
|
||||||
unit: 'none',
|
|
||||||
stat: 'avg',
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
formatWithMappings(mappings, value) {
|
|
||||||
const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
|
|
||||||
const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
|
|
||||||
|
|
||||||
const valueMap = valueMaps.map(mapping => {
|
|
||||||
if (mapping.value && value === mapping.value) {
|
|
||||||
return mapping.text;
|
|
||||||
}
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
const rangeMap = rangeMaps.map(mapping => {
|
|
||||||
if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
|
|
||||||
return mapping.text;
|
|
||||||
}
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
return { rangeMap, valueMap };
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValue(value) {
|
|
||||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
|
||||||
|
|
||||||
const formatFunc = kbn.valueFormats[unit];
|
|
||||||
const formattedValue = formatFunc(value, decimals);
|
|
||||||
|
|
||||||
if (valueMappings.length > 0) {
|
|
||||||
const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
|
|
||||||
|
|
||||||
if (valueMap) {
|
|
||||||
return `${prefix} ${valueMap} ${suffix}`;
|
|
||||||
} else if (rangeMap) {
|
|
||||||
return `${prefix} ${rangeMap} ${suffix}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(value)) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${prefix} ${formattedValue} ${suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFontColor(value) {
|
|
||||||
const { baseColor, maxValue, thresholds } = this.props;
|
|
||||||
|
|
||||||
if (thresholds.length > 0) {
|
|
||||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
|
||||||
|
|
||||||
if (atThreshold.length > 0) {
|
|
||||||
return atThreshold[0].color;
|
|
||||||
} else if (value <= maxValue) {
|
|
||||||
return BasicGaugeColor.Red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw() {
|
|
||||||
const {
|
|
||||||
baseColor,
|
|
||||||
maxValue,
|
|
||||||
minValue,
|
|
||||||
timeSeries,
|
|
||||||
showThresholdLabels,
|
|
||||||
showThresholdMarkers,
|
|
||||||
thresholds,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
stat,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let value: string | number = '';
|
|
||||||
|
|
||||||
if (timeSeries[0]) {
|
|
||||||
value = timeSeries[0].stats[stat];
|
|
||||||
} else {
|
|
||||||
value = 'N/A';
|
|
||||||
}
|
|
||||||
|
|
||||||
const dimension = Math.min(width, height * 1.3);
|
|
||||||
const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
|
||||||
const fontScale = parseInt('80', 10) / 100;
|
|
||||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
|
||||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
|
||||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
|
||||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
|
||||||
const thresholdLabelFontSize = fontSize / 2.5;
|
|
||||||
|
|
||||||
const formattedThresholds = [
|
|
||||||
{ value: minValue, color: BasicGaugeColor.Green },
|
|
||||||
...thresholds.map((threshold, index) => {
|
|
||||||
return {
|
|
||||||
value: threshold.value,
|
|
||||||
color: index === 0 ? threshold.color : thresholds[index].color,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
{ value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
|
|
||||||
];
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
series: {
|
|
||||||
gauges: {
|
|
||||||
gauge: {
|
|
||||||
min: minValue,
|
|
||||||
max: maxValue,
|
|
||||||
background: { color: backgroundColor },
|
|
||||||
border: { color: null },
|
|
||||||
shadow: { show: false },
|
|
||||||
width: gaugeWidth,
|
|
||||||
},
|
|
||||||
frame: { show: false },
|
|
||||||
label: { show: false },
|
|
||||||
layout: { margin: 0, thresholdWidth: 0 },
|
|
||||||
cell: { border: { width: 0 } },
|
|
||||||
threshold: {
|
|
||||||
values: formattedThresholds,
|
|
||||||
label: {
|
|
||||||
show: showThresholdLabels,
|
|
||||||
margin: thresholdMarkersWidth + 1,
|
|
||||||
font: { size: thresholdLabelFontSize },
|
|
||||||
},
|
|
||||||
show: showThresholdMarkers,
|
|
||||||
width: thresholdMarkersWidth,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
color: this.getFontColor(value),
|
|
||||||
formatter: () => {
|
|
||||||
return this.formatValue(value);
|
|
||||||
},
|
|
||||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
|
||||||
},
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const plotSeries = { data: [[0, value]] };
|
|
||||||
|
|
||||||
try {
|
|
||||||
$.plot(this.canvasElement, [plotSeries], options);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Gauge rendering error', err, options, timeSeries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { height, width } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="singlestat-panel">
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: `${height * 0.9}px`,
|
|
||||||
width: `${Math.min(width, height * 1.3)}px`,
|
|
||||||
top: '10px',
|
|
||||||
margin: 'auto',
|
|
||||||
}}
|
|
||||||
ref={element => (this.canvasElement = element)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Gauge;
|
|
@ -1,168 +0,0 @@
|
|||||||
// Libraries
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { colors } from '@grafana/ui';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
timeSeries: TimeSeries[];
|
|
||||||
nullValueMode: NullValueMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
|
|
||||||
const vmSeries = timeSeries.map((item, index) => {
|
|
||||||
const colorIndex = index % colors.length;
|
|
||||||
const label = item.target;
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
// stat defaults
|
|
||||||
let total = 0;
|
|
||||||
let max = -Number.MAX_VALUE;
|
|
||||||
let min = Number.MAX_VALUE;
|
|
||||||
let logmin = Number.MAX_VALUE;
|
|
||||||
let avg = null;
|
|
||||||
let current = null;
|
|
||||||
let first = null;
|
|
||||||
let delta = 0;
|
|
||||||
let diff = null;
|
|
||||||
let range = null;
|
|
||||||
let timeStep = Number.MAX_VALUE;
|
|
||||||
let allIsNull = true;
|
|
||||||
let allIsZero = true;
|
|
||||||
|
|
||||||
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
|
||||||
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
|
||||||
|
|
||||||
let currentTime;
|
|
||||||
let currentValue;
|
|
||||||
let nonNulls = 0;
|
|
||||||
let previousTime;
|
|
||||||
let previousValue = 0;
|
|
||||||
let previousDeltaUp = true;
|
|
||||||
|
|
||||||
for (let i = 0; i < item.datapoints.length; i++) {
|
|
||||||
currentValue = item.datapoints[i][0];
|
|
||||||
currentTime = item.datapoints[i][1];
|
|
||||||
|
|
||||||
// Due to missing values we could have different timeStep all along the series
|
|
||||||
// so we have to find the minimum one (could occur with aggregators such as ZimSum)
|
|
||||||
if (previousTime !== undefined) {
|
|
||||||
const currentStep = currentTime - previousTime;
|
|
||||||
if (currentStep < timeStep) {
|
|
||||||
timeStep = currentStep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previousTime = currentTime;
|
|
||||||
|
|
||||||
if (currentValue === null) {
|
|
||||||
if (ignoreNulls) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (nullAsZero) {
|
|
||||||
currentValue = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue !== null) {
|
|
||||||
if (_.isNumber(currentValue)) {
|
|
||||||
total += currentValue;
|
|
||||||
allIsNull = false;
|
|
||||||
nonNulls++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue > max) {
|
|
||||||
max = currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue < min) {
|
|
||||||
min = currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first === null) {
|
|
||||||
first = currentValue;
|
|
||||||
} else {
|
|
||||||
if (previousValue > currentValue) {
|
|
||||||
// counter reset
|
|
||||||
previousDeltaUp = false;
|
|
||||||
if (i === item.datapoints.length - 1) {
|
|
||||||
// reset on last
|
|
||||||
delta += currentValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (previousDeltaUp) {
|
|
||||||
delta += currentValue - previousValue; // normal increment
|
|
||||||
} else {
|
|
||||||
delta += currentValue; // account for counter reset
|
|
||||||
}
|
|
||||||
previousDeltaUp = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
previousValue = currentValue;
|
|
||||||
|
|
||||||
if (currentValue < logmin && currentValue > 0) {
|
|
||||||
logmin = currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue !== 0) {
|
|
||||||
allIsZero = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push([currentTime, currentValue]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max === -Number.MAX_VALUE) {
|
|
||||||
max = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (min === Number.MAX_VALUE) {
|
|
||||||
min = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.length && !allIsNull) {
|
|
||||||
avg = total / nonNulls;
|
|
||||||
current = result[result.length - 1][1];
|
|
||||||
if (current === null && result.length > 1) {
|
|
||||||
current = result[result.length - 2][1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max !== null && min !== null) {
|
|
||||||
range = max - min;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current !== null && first !== null) {
|
|
||||||
diff = current - first;
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = result.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: result,
|
|
||||||
label: label,
|
|
||||||
color: colors[colorIndex],
|
|
||||||
stats: {
|
|
||||||
total,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
current,
|
|
||||||
logmin,
|
|
||||||
avg,
|
|
||||||
diff,
|
|
||||||
delta,
|
|
||||||
timeStep,
|
|
||||||
range,
|
|
||||||
count,
|
|
||||||
first,
|
|
||||||
allIsZero,
|
|
||||||
allIsNull,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return vmSeries;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user