Commit d7c76dac by Ryan McKinley Committed by Torkel Ödegaard

ValueFormats: dynamically create units (#20763)

* update fixed

* update fixed

* update fixed

* don't change any tests

* add mising space

* Custom unit formats

* return a string for kbn

* return a string for kbn

* return a string for kbn

* Simplify unit tests

* More units

* fix more tests

* fix more tests

* fix more tests

* format values

* format values

* TimeSeries to string

* more kbn tests

* use the formatted value

* BarGauge: Fixed font size calculations

* support prefix

* add si support

* avoid npe

* BarGauge/BigValue: value formatting

* fix some tests

* fix tests

* remove displayDateFormat

* another unicode char

* Graph: Use react unit picker

* Updated unit picker

* Fixed build errors

* more formatting

* graph2 tooltip formatting

* optional chaining
parent 3289ee8b
......@@ -151,7 +151,9 @@ describe('Format value', () => {
it('should use override decimals', () => {
const value = 100030303;
const instance = getDisplayProcessor({ config: { decimals: 2, unit: 'bytes' } });
expect(instance(value).text).toEqual('95.40 MiB');
const disp = instance(value);
expect(disp.text).toEqual('95.40');
expect(disp.suffix).toEqual(' MiB');
});
it('should return mapped value if there are matching value mappings', () => {
......@@ -172,25 +174,33 @@ describe('Format value', () => {
it('with value 1000 and unit short', () => {
const value = 1000;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.000 K');
const disp = instance(value);
expect(disp.text).toEqual('1.000');
expect(disp.suffix).toEqual(' K');
});
it('with value 1200 and unit short', () => {
const value = 1200;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.200 K');
const disp = instance(value);
expect(disp.text).toEqual('1.200');
expect(disp.suffix).toEqual(' K');
});
it('with value 1250 and unit short', () => {
const value = 1250;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.250 K');
const disp = instance(value);
expect(disp.text).toEqual('1.250');
expect(disp.suffix).toEqual(' K');
});
it('with value 10000000 and unit short', () => {
const value = 1000000;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.000 Mil');
const disp = instance(value);
expect(disp.text).toEqual('1.000');
expect(disp.suffix).toEqual(' Mil');
});
});
......@@ -222,7 +232,7 @@ describe('Date display options', () => {
type: FieldType.time,
isUtc: true,
config: {
dateDisplayFormat: 'YYYY',
unit: 'time:YYYY',
},
});
expect(processor(0).text).toEqual('1970');
......
......@@ -11,7 +11,7 @@ import { DisplayProcessor, DisplayValue, DecimalCount, DecimalInfo } from '../ty
import { getValueFormat } from '../valueFormats/valueFormats';
import { getMappedValue } from '../utils/valueMappings';
import { Threshold } from '../types/threshold';
import { DateTime, DEFAULT_DATE_TIME_FORMAT, isDateTime, dateTime, toUtc } from '../datetime';
import { DEFAULT_DATE_TIME_FORMAT } from '../datetime';
import { KeyValue } from '../types';
interface DisplayProcessorOptions {
......@@ -37,22 +37,10 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (options.type === FieldType.time) {
if (field.unit && timeFormats[field.unit]) {
// Currently selected unit is valid for time fields
} else if (field.unit && field.unit.startsWith('time:')) {
// Also OK
} else {
const dateFormat = field.dateDisplayFormat || DEFAULT_DATE_TIME_FORMAT;
// UTC or browser based timezone
let fmt = (date: DateTime) => date.format(dateFormat);
if (options.isUtc) {
fmt = (date: DateTime) => toUtc(date).format(dateFormat);
}
return (value: any) => {
const date: DateTime = isDateTime(value) ? value : dateTime(value);
return {
numeric: isNaN(value) ? date.valueOf() : value,
text: fmt(date),
};
};
field.unit = `time:${DEFAULT_DATE_TIME_FORMAT}`;
}
}
......@@ -65,6 +53,8 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
let text = _.toString(value);
let numeric = toNumber(value);
let prefix: string | undefined = undefined;
let suffix: string | undefined = undefined;
let shouldFormat = true;
if (mappings && mappings.length > 0) {
......@@ -85,7 +75,10 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
const v = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
text = v.text;
suffix = v.suffix;
prefix = v.prefix;
// Check if the formatted text mapped to a different value
if (mappings && mappings.length > 0) {
......@@ -107,7 +100,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
text = ''; // No data?
}
}
return { text, numeric, color };
return { text, numeric, color, prefix, suffix };
};
}
......
......@@ -286,8 +286,12 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display
text: '',
};
let prefixLength = 0;
let suffixLength = 0;
for (let i = 0; i < values.length; i++) {
const v = values[i].display;
if (v.text && v.text.length > info.text.length) {
info.text = v.text;
}
......@@ -295,6 +299,16 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display
if (v.title && v.title.length > info.title.length) {
info.title = v.title;
}
if (v.prefix && v.prefix.length > prefixLength) {
info.prefix = v.prefix;
prefixLength = v.prefix.length;
}
if (v.suffix && v.suffix.length > suffixLength) {
info.suffix = v.suffix;
suffixLength = v.suffix.length;
}
}
return info;
}
......@@ -46,9 +46,6 @@ export interface FieldConfig {
// Visual options
color?: string;
// Used for time field formatting
dateDisplayFormat?: string;
}
export interface Field<T = any, V = Vector<T>> {
......
import { FormattedValue } from '../valueFormats';
export type DisplayProcessor = (value: any) => DisplayValue;
export interface DisplayValue {
text: string; // Show in the UI
export interface DisplayValue extends FormattedValue {
numeric: number; // Use isNaN to check if it is a real number
color?: string; // color based on configs or Threshold
title?: string;
fontSize?: string;
}
/**
* These represents the displau value with the longest title and text.
* These represents the display value with the longest title and text.
* Used to align widths and heights when displaying multiple DisplayValues
*/
export interface DisplayValueAlignmentFactors {
export interface DisplayValueAlignmentFactors extends FormattedValue {
title: string;
text: string;
}
export type DecimalCount = number | null | undefined;
......
import { toHex, toHex0x } from './arithmeticFormatters';
import { formattedValueToString } from './valueFormats';
describe('hex', () => {
it('positive integer', () => {
const str = toHex(100, 0);
expect(str).toBe('64');
expect(formattedValueToString(str)).toBe('64');
});
it('negative integer', () => {
const str = toHex(-100, 0);
expect(str).toBe('-64');
expect(formattedValueToString(str)).toBe('-64');
});
it('positive float', () => {
const str = toHex(50.52, 1);
expect(str).toBe('32.8');
expect(formattedValueToString(str)).toBe('32.8');
});
it('negative float', () => {
const str = toHex(-50.333, 2);
expect(str).toBe('-32.547AE147AE14');
expect(formattedValueToString(str)).toBe('-32.547AE147AE14');
});
});
describe('hex 0x', () => {
it('positive integeter', () => {
const str = toHex0x(7999, 0);
expect(str).toBe('0x1F3F');
expect(formattedValueToString(str)).toBe('0x1F3F');
});
it('negative integer', () => {
const str = toHex0x(-584, 0);
expect(str).toBe('-0x248');
expect(formattedValueToString(str)).toBe('-0x248');
});
it('positive float', () => {
const str = toHex0x(74.443, 3);
expect(str).toBe('0x4A.716872B020C4');
expect(formattedValueToString(str)).toBe('0x4A.716872B020C4');
});
it('negative float', () => {
const str = toHex0x(-65.458, 1);
expect(str).toBe('-0x41.8');
expect(formattedValueToString(str)).toBe('-0x41.8');
});
});
import { toFixed } from './valueFormats';
import { toFixed, FormattedValue } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
export function toPercent(size: number, decimals: DecimalCount) {
export function toPercent(size: number, decimals: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
return toFixed(size, decimals) + '%';
return { text: toFixed(size, decimals), suffix: '%' };
}
export function toPercentUnit(size: number, decimals: DecimalCount) {
export function toPercentUnit(size: number, decimals: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
return toFixed(100 * size, decimals) + '%';
return { text: toFixed(100 * size, decimals), suffix: '%' };
}
export function toHex0x(value: number, decimals: DecimalCount) {
export function toHex0x(value: number, decimals: DecimalCount): FormattedValue {
if (value == null) {
return '';
return { text: '' };
}
const hexString = toHex(value, decimals);
if (hexString.substring(0, 1) === '-') {
return '-0x' + hexString.substring(1);
const asHex = toHex(value, decimals);
if (asHex.text.substring(0, 1) === '-') {
asHex.text = '-0x' + asHex.text.substring(1);
} else {
asHex.text = '0x' + asHex.text;
}
return '0x' + hexString;
return asHex;
}
export function toHex(value: number, decimals: DecimalCount) {
export function toHex(value: number, decimals: DecimalCount): FormattedValue {
if (value == null) {
return '';
return { text: '' };
}
return parseFloat(toFixed(value, decimals))
.toString(16)
.toUpperCase();
return {
text: parseFloat(toFixed(value, decimals))
.toString(16)
.toUpperCase(),
};
}
export function sci(value: number, decimals: DecimalCount) {
export function sci(value: number, decimals: DecimalCount): FormattedValue {
if (value == null) {
return '';
return { text: '' };
}
return value.toExponential(decimals as number);
return { text: value.toExponential(decimals as number) };
}
import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats';
import { locale, scaledUnits, simpleCountUnit, toFixedUnit, ValueFormatCategory } from './valueFormats';
import {
dateTimeAsIso,
dateTimeAsUS,
......@@ -24,7 +24,7 @@ export const getCategories = (): ValueFormatCategory[] => [
{
name: 'Misc',
formats: [
{ name: 'none', id: 'none', fn: toFixed },
{ name: 'none', id: 'none', fn: toFixedUnit('') },
{
name: 'short',
id: 'short',
......@@ -107,10 +107,10 @@ export const getCategories = (): ValueFormatCategory[] => [
{ name: 'Rubles (₽)', id: 'currencyRUB', fn: currency('₽') },
{ name: 'Hryvnias (₴)', id: 'currencyUAH', fn: currency('₴') },
{ name: 'Real (R$)', id: 'currencyBRL', fn: currency('R$') },
{ name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr') },
{ name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr') },
{ name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr') },
{ name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr') },
{ name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr', true) },
{ name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr', true) },
{ name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr', true) },
{ name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr', true) },
{ name: 'Czech koruna (czk)', id: 'currencyCZK', fn: currency('czk') },
{ name: 'Swiss franc (CHF)', id: 'currencyCHF', fn: currency('CHF') },
{ name: 'Polish Złoty (PLN)', id: 'currencyPLN', fn: currency('PLN') },
......
import { currency } from './symbolFormatters';
describe('Currency', () => {
it('should format as usd', () => {
expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K');
});
it('should format as krw', () => {
expect(currency('₩')(1532.82, 1, -1)).toEqual('₩1.53K');
});
});
import { scaledUnits } from './valueFormats';
import { scaledUnits, ValueFormatter } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
export function currency(symbol: string) {
export function currency(symbol: string, asSuffix?: boolean): ValueFormatter {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
return { text: '' };
}
const scaled = scaler(size, decimals, scaledDecimals);
return symbol + scaled;
if (asSuffix) {
scaled.suffix = symbol;
} else {
scaled.prefix = symbol;
}
return scaled;
};
}
export function binarySIPrefix(unit: string, offset = 0) {
export function getOffsetFromSIPrefix(c: string): number {
switch (c) {
case 'f':
return -5;
case 'p':
return -4;
case 'n':
return -3;
case 'μ': // Two different unicode chars for µ
case 'µ':
return -2;
case 'm':
return -1;
case '':
return 0;
case 'k':
return 1;
case 'M':
return 2;
case 'G':
return 3;
case 'T':
return 4;
case 'P':
return 5;
case 'E':
return 6;
case 'Z':
return 7;
case 'Y':
return 8;
}
return 0;
}
export function binarySIPrefix(unit: string, offset = 0): ValueFormatter {
const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset);
const units = prefixes.map(p => {
return ' ' + p + unit;
......@@ -21,7 +61,7 @@ export function binarySIPrefix(unit: string, offset = 0) {
return scaledUnits(1024, units);
}
export function decimalSIPrefix(unit: string, offset = 0) {
export function decimalSIPrefix(unit: string, offset = 0): ValueFormatter {
let prefixes = ['f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
prefixes = prefixes.slice(5 + (offset || 0));
const units = prefixes.map(p => {
......
import { toFixed, getValueFormat, scaledUnits } from './valueFormats';
import { toFixed, getValueFormat, scaledUnits, formattedValueToString } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
import { TimeZone } from '../types';
import { dateTime } from '../datetime';
interface ValueFormatTest {
id: string;
decimals?: DecimalCount;
scaledDecimals?: DecimalCount;
timeZone?: TimeZone;
value: number;
result: string;
}
const formatTests: ValueFormatTest[] = [
// Currancy
{ id: 'currencyUSD', decimals: 2, value: 1532.82, result: '$1.53K' },
{ id: 'currencyKRW', decimals: 2, value: 1532.82, result: '₩1.53K' },
// Standard
{ id: 'ms', decimals: 4, value: 0.0024, result: '0.0024 ms' },
{ id: 'ms', decimals: 0, value: 100, result: '100 ms' },
{ id: 'ms', decimals: 2, value: 1250, result: '1.25 s' },
{ id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' },
{ id: 'ms', decimals: 0, value: 1200, result: '1 s' },
{ id: 'short', decimals: 0, scaledDecimals: -1, value: 98765, result: '98.77 K' },
{ id: 'short', decimals: 0, scaledDecimals: 0, value: 9876543, result: '9.876543 Mil' },
{ id: 'kbytes', decimals: 3, value: 10000000, result: '9.537 GiB' },
{ id: 'deckbytes', decimals: 3, value: 10000000, result: '10.000 GB' },
{ id: 'megwatt', decimals: 3, value: 1000, result: '1.000 GW' },
{ id: 'kohm', decimals: 3, value: 1000, result: '1.000 MΩ' },
{ id: 'Mohm', decimals: 3, value: 1000, result: '1.000 GΩ' },
{ id: 'farad', decimals: 3, value: 1000, result: '1.000 kF' },
{ id: 'µfarad', decimals: 3, value: 1000, result: '1.000 mF' },
{ id: 'nfarad', decimals: 3, value: 1000, result: '1.000 µF' },
{ id: 'pfarad', decimals: 3, value: 1000, result: '1.000 nF' },
{ id: 'ffarad', decimals: 3, value: 1000, result: '1.000 pF' },
{ id: 'henry', decimals: 3, value: 1000, result: '1.000 kH' },
{ id: 'mhenry', decimals: 3, value: 1000, result: '1.000 H' },
{ id: 'µhenry', decimals: 3, value: 1000, result: '1.000 mH' },
// Suffix (unknown units append to the end)
{ id: 'a', decimals: 0, value: 1532.82, result: '1533 a' },
{ id: 'b', decimals: 0, value: 1532.82, result: '1533 b' },
// Prefix (unknown units append to the end)
{ id: 'prefix:b', value: 1532.82, result: 'b1533' },
// SI Units
{ id: 'si:µF', value: 1234, decimals: 2, result: '1.23 mF' },
{ id: 'si:µF', value: 1234000000, decimals: 2, result: '1.23 kF' },
{ id: 'si:µF', value: 1234000000000000, decimals: 2, result: '1.23 GF' },
// Time format
{ id: 'time:YYYY', decimals: 0, value: dateTime(new Date(1999, 6, 2)).valueOf(), result: '1999' },
{ id: 'time:YYYY.MM', decimals: 0, value: dateTime(new Date(2010, 6, 2)).valueOf(), result: '2010.07' },
];
describe('valueFormats', () => {
it('Manually check a format', () => {
// helpful for adding tests one at a time with the debugger
const tests: ValueFormatTest[] = [
{ id: 'time:YYYY.MM', decimals: 0, value: dateTime(new Date(2010, 6, 2)).valueOf(), result: '2010.07' },
];
const test = tests[0];
const result = getValueFormat(test.id)(test.value, test.decimals, test.scaledDecimals);
const full = formattedValueToString(result);
expect(full).toBe(test.result);
});
for (const test of formatTests) {
describe(`value format: ${test.id}`, () => {
it(`should translate ${test.value} as ${test.result}`, () => {
const result = getValueFormat(test.id)(test.value, test.decimals, test.scaledDecimals);
const full = formattedValueToString(result);
expect(full).toBe(test.result);
});
});
}
describe('normal cases', () => {
it('toFixed should handle number correctly if decimal is null', () => {
expect(toFixed(100)).toBe('100');
......@@ -18,28 +97,6 @@ describe('valueFormats', () => {
expect(toFixed(100.4, 2)).toBe('100.40');
expect(toFixed(100.5, 2)).toBe('100.50');
});
it('scaledUnit should handle number correctly if scaledDecimals is not null', () => {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
expect(scaler(98765, 0, 0)).toBe('98.765K');
expect(scaler(98765, 0, -1)).toBe('98.77K');
expect(scaler(9876543, 0, 0)).toBe('9.876543M');
expect(scaler(9876543, 0, -1)).toBe('9.87654M');
});
it('scaledUnit should handle number correctly if scaledDecimals is null', () => {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
expect(scaler(98765, 1, null)).toBe('98.8K');
expect(scaler(98765, 2, null)).toBe('98.77K');
expect(scaler(9876543, 2, null)).toBe('9.88M');
expect(scaler(9876543, 3, null)).toBe('9.877M');
});
});
describe('format edge cases', () => {
......@@ -54,9 +111,9 @@ describe('valueFormats', () => {
it('scaledUnits should handle non number input gracefully', () => {
const disp = scaledUnits(5, ['a', 'b', 'c']);
expect(disp(NaN)).toBe('NaN');
expect(disp(Number.NEGATIVE_INFINITY)).toBe(negInf);
expect(disp(Number.POSITIVE_INFINITY)).toBe(posInf);
expect(disp(NaN).text).toBe('NaN');
expect(disp(Number.NEGATIVE_INFINITY).text).toBe(negInf);
expect(disp(Number.POSITIVE_INFINITY).text).toBe(posInf);
});
});
......@@ -66,109 +123,4 @@ describe('valueFormats', () => {
expect(str).toBe('186');
});
});
describe('ms format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('ms')(10000086.123, 1, null);
expect(str).toBe('2.8 hour');
});
});
describe('kbytes format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('kbytes')(10000000, 3, null);
expect(str).toBe('9.537 GiB');
});
});
describe('deckbytes format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('deckbytes')(10000000, 3, null);
expect(str).toBe('10.000 GB');
});
});
describe('ms format when scaled decimals is 0', () => {
it('should use scaledDecimals and add 3', () => {
const str = getValueFormat('ms')(1200, 0, 0);
expect(str).toBe('1.200 s');
});
});
describe('megawatt format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('megwatt')(1000, 3, null);
expect(str).toBe('1.000 GW');
});
});
describe('kiloohm format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('kohm')(1000, 3, null);
expect(str).toBe('1.000 MΩ');
});
});
describe('megaohm format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('Mohm')(1000, 3, null);
expect(str).toBe('1.000 GΩ');
});
});
describe('farad format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('farad')(1000, 3, null);
expect(str).toBe('1.000 kF');
});
});
describe('microfarad format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('µfarad')(1000, 3, null);
expect(str).toBe('1.000 mF');
});
});
describe('nanofarad format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('nfarad')(1000, 3, null);
expect(str).toBe('1.000 µF');
});
});
describe('picofarad format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('pfarad')(1000, 3, null);
expect(str).toBe('1.000 nF');
});
});
describe('femtofarad format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('ffarad')(1000, 3, null);
expect(str).toBe('1.000 pF');
});
});
describe('henry format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('henry')(1000, 3, null);
expect(str).toBe('1.000 kH');
});
});
describe('millihenry format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('mhenry')(1000, 3, null);
expect(str).toBe('1.000 H');
});
});
describe('microhenry format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('µhenry')(1000, 3, null);
expect(str).toBe('1.000 mH');
});
});
});
import { getCategories } from './categories';
import { DecimalCount } from '../types/displayValue';
import { toDateTimeValueFormatter } from './dateTimeFormatters';
import { getOffsetFromSIPrefix, decimalSIPrefix } from './symbolFormatters';
export interface FormattedValue {
text: string;
prefix?: string;
suffix?: string;
}
export function formattedValueToString(val: FormattedValue): string {
return `${val.prefix ?? ''}${val.text}${val.suffix ?? ''}`;
}
export type ValueFormatter = (
value: number,
decimals?: DecimalCount,
scaledDecimals?: DecimalCount,
isUtc?: boolean
) => string;
isUtc?: boolean // TODO: timezone?: string,
) => FormattedValue;
export interface ValueFormat {
name: string;
......@@ -63,32 +75,42 @@ export function toFixedScaled(
scaledDecimals: DecimalCount,
additionalDecimals: number,
ext?: string
) {
): FormattedValue {
if (scaledDecimals === null || scaledDecimals === undefined) {
return toFixed(value, decimals) + ext;
return { text: toFixed(value, decimals), suffix: ext };
}
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
return {
text: toFixed(value, scaledDecimals + additionalDecimals),
suffix: ext,
};
}
export function toFixedUnit(unit: string): ValueFormatter {
export function toFixedUnit(unit: string, asPrefix?: boolean): ValueFormatter {
return (size: number, decimals?: DecimalCount) => {
if (size === null) {
return '';
return { text: '' };
}
const text = toFixed(size, decimals);
if (unit) {
if (asPrefix) {
return { text, prefix: unit };
}
return { text, suffix: ' ' + unit };
}
return toFixed(size, decimals) + ' ' + unit;
return { text };
};
}
// Formatter which scales the unit string geometrically according to the given
// numeric factor. Repeatedly scales the value down by the factor until it is
// less than the factor in magnitude, or the end of the array is reached.
export function scaledUnits(factor: number, extArray: string[]) {
export function scaledUnits(factor: number, extArray: string[]): ValueFormatter {
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
return { text: '' };
}
if (size === Number.NEGATIVE_INFINITY || size === Number.POSITIVE_INFINITY || isNaN(size)) {
return size.toLocaleString();
return { text: size.toLocaleString() };
}
let steps = 0;
......@@ -99,7 +121,7 @@ export function scaledUnits(factor: number, extArray: string[]) {
size /= factor;
if (steps >= limit) {
return 'NA';
return { text: 'NA' };
}
}
......@@ -107,26 +129,29 @@ export function scaledUnits(factor: number, extArray: string[]) {
decimals = scaledDecimals + 3 * steps;
}
return toFixed(size, decimals) + extArray[steps];
return { text: toFixed(size, decimals), suffix: extArray[steps] };
};
}
export function locale(value: number, decimals: DecimalCount) {
export function locale(value: number, decimals: DecimalCount): FormattedValue {
if (value == null) {
return '';
return { text: '' };
}
return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number });
return {
text: value.toLocaleString(undefined, { maximumFractionDigits: decimals as number }),
};
}
export function simpleCountUnit(symbol: string) {
export function simpleCountUnit(symbol: string): ValueFormatter {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
return { text: '' };
}
const scaled = scaler(size, decimals, scaledDecimals);
return scaled + ' ' + symbol;
const v = scaler(size, decimals, scaledDecimals);
v.suffix += ' ' + symbol;
return v;
};
}
......@@ -147,7 +172,27 @@ export function getValueFormat(id: string): ValueFormatter {
buildFormats();
}
return index[id];
const fmt = index[id];
if (!fmt && id) {
const idx = id.indexOf(':');
if (idx > 0) {
const key = id.substring(0, idx);
const sub = id.substring(idx + 1);
if (key === 'prefix') {
return toFixedUnit(sub, true);
}
if (key === 'time') {
return toDateTimeValueFormatter(sub);
}
if (key === 'si') {
const offset = getOffsetFromSIPrefix(sub.charAt(0));
const unit = offset === 0 ? sub : sub.substring(1);
return decimalSIPrefix(unit, offset);
}
}
return toFixedUnit(id);
}
return fmt;
}
export function getValueFormatterIndex(): ValueFormatterIndex {
......
......@@ -15,7 +15,6 @@ import { getTheme } from '../../themes';
const green = '#73BF69';
const orange = '#FF9830';
// const red = '#BB';
function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
......
......@@ -6,9 +6,14 @@ import {
TimeSeriesValue,
getActiveThreshold,
DisplayValue,
formattedValueToString,
FormattedValue,
DisplayValueAlignmentFactors,
} from '@grafana/data';
// Compontents
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
// Utils
import { getColorFromHexRgbOrName } from '@grafana/data';
import { measureText, calculateFontSize } from '../../utils/measureText';
......@@ -93,9 +98,7 @@ export class BarGauge extends PureComponent<Props> {
return (
<div style={styles.wrapper}>
<div className="bar-gauge__value" style={styles.value}>
{value.text}
</div>
<FormattedValueDisplay className="bar-gauge__value" value={value} style={styles.value} />
<div style={styles.bar} />
</div>
);
......@@ -165,8 +168,8 @@ export class BarGauge extends PureComponent<Props> {
const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount);
const valueColor = getValueColor(this.props);
const valueTextToBaseSizeOn = alignmentFactors ? alignmentFactors.text : value.text;
const valueStyles = getValueStyles(valueTextToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation);
const containerStyles: CSSProperties = {
width: `${wrapperWidth}px`,
......@@ -180,6 +183,7 @@ export class BarGauge extends PureComponent<Props> {
} else {
containerStyles.flexDirection = 'row';
containerStyles.alignItems = 'center';
valueStyles.justifyContent = 'flex-end';
}
const cells: JSX.Element[] = [];
......@@ -213,9 +217,7 @@ export class BarGauge extends PureComponent<Props> {
return (
<div style={containerStyles}>
{cells}
<div className="bar-gauge__value" style={valueStyles}>
{value.text}
</div>
<FormattedValueDisplay className="bar-gauge__value" value={value} style={valueStyles} />
</div>
);
}
......@@ -394,8 +396,8 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
const valueColor = getValueColor(props);
const valueTextToBaseSizeOn = alignmentFactors ? alignmentFactors.text : value.text;
const valueStyles = getValueStyles(valueTextToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation);
const isBasic = displayMode === 'basic';
const wrapperStyles: CSSProperties = {
......@@ -504,13 +506,13 @@ export function getValueColor(props: Props): string {
}
function getValueStyles(
value: string,
value: FormattedValue,
color: string,
width: number,
height: number,
orientation: VizOrientation
): CSSProperties {
const valueStyles: CSSProperties = {
const styles: CSSProperties = {
color: color,
height: `${height}px`,
width: `${width}px`,
......@@ -523,14 +525,16 @@ function getValueStyles(
let textWidth = width;
if (isVertical(orientation)) {
valueStyles.justifyContent = `center`;
styles.justifyContent = `center`;
} else {
valueStyles.justifyContent = `flex-start`;
valueStyles.paddingLeft = `${VALUE_LEFT_PADDING}px`;
styles.justifyContent = `flex-start`;
styles.paddingLeft = `${VALUE_LEFT_PADDING}px`;
// Need to remove the left padding from the text width constraints
textWidth -= VALUE_LEFT_PADDING;
}
valueStyles.fontSize = calculateFontSize(value, textWidth, height, VALUE_LINE_HEIGHT) + 'px';
return valueStyles;
const formattedValueString = formattedValueToString(value);
styles.fontSize = calculateFontSize(formattedValueString, textWidth, height, VALUE_LINE_HEIGHT);
return styles;
}
......@@ -20,14 +20,14 @@ exports[`BarGauge Render with basic options should render 1`] = `
}
}
>
<div
<FormattedDisplayValue
className="bar-gauge__value"
style={
Object {
"alignItems": "center",
"color": "#73BF69",
"display": "flex",
"fontSize": "175px",
"fontSize": 175,
"height": "300px",
"justifyContent": "flex-start",
"lineHeight": 1,
......@@ -35,9 +35,13 @@ exports[`BarGauge Render with basic options should render 1`] = `
"width": "60px",
}
}
>
25
</div>
value={
Object {
"numeric": 25,
"text": "25",
}
}
/>
<div
style={
Object {
......
......@@ -11,7 +11,9 @@ import {
getValueStyles,
getTitleStyles,
} from './styles';
import { renderGraph } from './renderGraph';
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
export interface BigValueSparkline {
data: GraphSeriesValue[][];
......@@ -67,26 +69,10 @@ export class BigValue extends PureComponent<Props> {
<div className={className} style={panelStyles} onClick={onClick}>
<div style={valueAndTitleContainerStyles}>
{value.title && <div style={titleStyles}>{value.title}</div>}
<div style={valueStyles}>{renderValueWithSmallerUnit(value.text, layout.valueFontSize)}</div>
<FormattedValueDisplay value={value} style={valueStyles} />
</div>
{renderGraph(layout, sparkline)}
</div>
);
}
}
function renderValueWithSmallerUnit(value: string, fontSize: number) {
const valueParts = value.split(' ');
const unitSize = `${fontSize * 0.7}px`;
if (valueParts.length === 2) {
return (
<>
{valueParts[0]}
<span style={{ fontSize: unitSize, paddingLeft: '2px' }}>{valueParts[1]}</span>
</>
);
}
return value;
}
......@@ -27,19 +27,23 @@ exports[`BigValue Render with basic options should render 1`] = `
}
}
>
<div
<FormattedDisplayValue
style={
Object {
"color": "#EEE",
"fontSize": "230px",
"fontSize": 230,
"fontWeight": 500,
"lineHeight": 1.2,
"textShadow": "#333 0px 0px 1px",
}
}
>
25
</div>
value={
Object {
"numeric": 25,
"text": "25",
}
}
/>
</div>
</div>
`;
......@@ -3,7 +3,7 @@ import { CSSProperties } from 'react';
import tinycolor from 'tinycolor2';
// Utils
import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data';
import { getColorFromHexRgbOrName, GrafanaTheme, formattedValueToString } from '@grafana/data';
import { calculateFontSize } from '../../utils/measureText';
// Types
......@@ -49,7 +49,7 @@ export function calculateLayout(props: Props): LayoutResult {
const justifyCenter = shouldJustifyCenter(props);
const panelPadding = height > 100 ? 12 : 8;
const titleToAlignTo = alignmentFactors ? alignmentFactors.title : value.title;
const valueToAlignTo = alignmentFactors ? alignmentFactors.text : value.text;
const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : value);
const maxTitleFontSize = 30;
const maxTextWidth = width - panelPadding * 2;
......@@ -186,7 +186,7 @@ export function getTitleStyles(layout: LayoutResult) {
export function getValueStyles(layout: LayoutResult) {
const styles: CSSProperties = {
fontSize: `${layout.valueFontSize}px`,
fontSize: layout.valueFontSize,
color: '#EEE',
textShadow: '#333 0px 0px 1px',
fontWeight: 500,
......
import React, { FC, CSSProperties } from 'react';
import { FormattedValue } from '@grafana/data';
export interface Props {
className?: string;
value: FormattedValue;
style: CSSProperties;
}
function fontSizeReductionFactor(fontSize: number) {
if (fontSize < 20) {
return 0.9;
}
if (fontSize < 26) {
return 0.8;
}
return 0.6;
}
export const FormattedValueDisplay: FC<Props> = ({ value, className, style }) => {
const fontSize = style.fontSize as number;
const reductionFactor = fontSizeReductionFactor(fontSize);
const hasPrefix = (value.prefix ?? '').length > 0;
const hasSuffix = (value.suffix ?? '').length > 0;
return (
<div className={className} style={style}>
<div>
{hasPrefix && <span>{value.prefix}</span>}
<span>{value.text}</span>
{hasSuffix && <span style={{ fontSize: fontSize * reductionFactor }}>{value.suffix}</span>}
</div>
</div>
);
};
FormattedValueDisplay.displayName = 'FormattedDisplayValue';
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { Threshold, DisplayValue } from '@grafana/data';
import { getColorFromHexRgbOrName } from '@grafana/data';
import { Threshold, DisplayValue, getColorFromHexRgbOrName, formattedValueToString } from '@grafana/data';
import { Themeable } from '../../types';
import { selectThemeVariant } from '../../themes';
......@@ -82,7 +80,8 @@ export class Gauge extends PureComponent<Props> {
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
const gaugeWidth = Math.min(dimension / 5.5, 40) / gaugeWidthReduceRatio;
const thresholdMarkersWidth = gaugeWidth / 5;
const fontSize = Math.min(dimension / 4, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
const text = formattedValueToString(value);
const fontSize = Math.min(dimension / 4, 100) * (text !== null ? this.getFontScale(text.length) : 1);
const thresholdLabelFontSize = fontSize / 2.5;
......@@ -114,7 +113,7 @@ export class Gauge extends PureComponent<Props> {
value: {
color: value.color,
formatter: () => {
return value.text;
return text;
},
font: { size: fontSize, family: theme.typography.fontFamily.sansSerif },
},
......
......@@ -6,7 +6,7 @@ import { SeriesColorChangeHandler } from './GraphWithLegend';
import { LegendStatsList } from '../Legend/LegendStatsList';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { GrafanaTheme, formattedValueToString } from '@grafana/data';
export interface GraphLegendItemProps {
key?: React.Key;
......@@ -124,7 +124,7 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
item.displayValues.map((stat, index) => {
return (
<td className={styles.value} key={`${stat.title}-${index}`}>
{stat.text}
{formattedValueToString(stat)}
</td>
);
})}
......
import React from 'react';
import { getValueFromDimension, getColumnFromDimension } from '@grafana/data';
import { getValueFromDimension, getColumnFromDimension, formattedValueToString } from '@grafana/data';
import { SeriesTable } from './SeriesTable';
import { GraphTooltipContentProps } from './types';
......@@ -15,11 +15,11 @@ export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({ dim
}
const time = getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]);
const timeField = getColumnFromDimension(dimensions.xAxis, activeDimensions.xAxis[0]);
const processedTime = timeField.display ? timeField.display(time).text : time;
const processedTime = timeField.display ? formattedValueToString(timeField.display(time)) : time;
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]);
const processedValue = valueField.display ? valueField.display(value).text : value;
const processedValue = valueField.display ? formattedValueToString(valueField.display(value)) : value;
return (
<SeriesTable
......
import { GraphSeriesValue, Field } from '@grafana/data';
import { GraphSeriesValue, Field, formattedValueToString } from '@grafana/data';
/**
* Returns index of the closest datapoint BEFORE hover position
......@@ -72,18 +72,18 @@ export const getMultiSeriesGraphHoverInfo = (
(hoverDistance < 0 && hoverDistance > minDistance)
) {
minDistance = hoverDistance;
minTime = time.display ? time.display(pointTime).text : pointTime;
minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime;
}
value = series.values.get(hoverIndex);
results.push({
value: series.display ? series.display(value).text : value,
value: series.display ? formattedValueToString(series.display(value)) : value,
datapointIndex: hoverIndex,
seriesIndex: i,
color: series.config.color,
label: series.name,
time: time.display ? time.display(pointTime).text : pointTime,
time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime,
});
}
......
import React from 'react';
import { InlineList } from '../List/InlineList';
import { css } from 'emotion';
import { DisplayValue } from '@grafana/data';
import { DisplayValue, formattedValueToString } from '@grafana/data';
import capitalize from 'lodash/capitalize';
const LegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat }) => {
......@@ -11,7 +11,7 @@ const LegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat
margin-left: 6px;
`}
>
{stat.title && `${capitalize(stat.title)}:`} {stat.text}
{stat.title && `${capitalize(stat.title)}:`} {formattedValueToString(stat)}
</div>
);
};
......
import React, { PureComponent } from 'react';
import { select, pie, arc, event } from 'd3';
import sum from 'lodash/sum';
import { DisplayValue, GrafanaThemeType } from '@grafana/data';
import { DisplayValue, GrafanaThemeType, formattedValueToString } from '@grafana/data';
import { Themeable } from '../../index';
import { colors as grafana_colors } from '../../utils/index';
......@@ -49,7 +49,7 @@ export class PieChart extends PureComponent<Props> {
}
const data = values.map(datapoint => datapoint.numeric);
const names = values.map(datapoint => datapoint.text);
const names = values.map(datapoint => formattedValueToString(datapoint));
const colors = values.map((p, idx) => {
if (p.color) {
return p.color;
......
......@@ -50,6 +50,7 @@ export interface CommonProps<T> {
onOpenMenu?: () => void;
onCloseMenu?: () => void;
tabSelectsValue?: boolean;
formatCreateLabel?: (input: string) => string;
allowCustomValue: boolean;
}
......@@ -125,6 +126,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
onCloseMenu,
onOpenMenu,
allowCustomValue,
formatCreateLabel,
} = this.props;
let widthClass = '';
......@@ -137,7 +139,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
if (allowCustomValue) {
SelectComponent = Creatable;
creatableOptions.formatCreateLabel = (input: string) => input;
creatableOptions.formatCreateLabel = formatCreateLabel ?? ((input: string) => input);
}
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
......
......@@ -13,7 +13,6 @@ import {
VAR_CALC,
VAR_CELL_PREFIX,
toIntegerOrUndefined,
SelectableValue,
FieldConfig,
toFloatOrUndefined,
toNumberString,
......@@ -62,8 +61,8 @@ export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMi
[value.max, onChange]
);
const onUnitChange = (unit: SelectableValue<string>) => {
onChange({ ...value, unit: unit.value });
const onUnitChange = (unit?: string) => {
onChange({ ...value, unit });
};
const commitChanges = useCallback(() => {
......@@ -102,7 +101,7 @@ export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMi
<div className="gf-form">
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={onUnitChange} />
<UnitPicker value={unit} onChange={onUnitChange} />
</div>
{showMinMax && (
<>
......
......@@ -12,6 +12,7 @@ import {
ValueFormatter,
getColorFromHexRgbOrName,
InterpolateFunction,
formattedValueToString,
} from '@grafana/data';
export interface TableCellBuilderOptions {
......@@ -316,7 +317,7 @@ export function getFieldCellBuilder(field: Field, style: ColumnStyle | null, p:
return (
<div style={style} className={clazz} title={disp.title}>
{disp.text}
{formattedValueToString(disp)}
</div>
);
};
......
......@@ -2,21 +2,29 @@ import React, { PureComponent } from 'react';
import { Select } from '../Select/Select';
import { getValueFormats } from '@grafana/data';
import { getValueFormats, SelectableValue } from '@grafana/data';
interface Props {
onChange: (item: any) => void;
defaultValue?: string;
onChange: (item?: string) => void;
value?: string;
width?: number;
}
function formatCreateLabel(input: string) {
return `Unit suffix: ${input}`;
}
export class UnitPicker extends PureComponent<Props> {
static defaultProps = {
width: 12,
};
onChange = (value: SelectableValue<string>) => {
this.props.onChange(value.value);
};
render() {
const { defaultValue, onChange, width } = this.props;
const { value, width } = this.props;
const unitGroups = getValueFormats();
......@@ -35,18 +43,20 @@ export class UnitPicker extends PureComponent<Props> {
};
});
const value = groupOptions.map(group => {
return group.options.find(option => option.value === defaultValue);
const valueOption = groupOptions.map(group => {
return group.options.find(option => option.value === value);
});
return (
<Select
width={width}
defaultValue={value}
defaultValue={valueOption}
isSearchable={true}
allowCustomValue={true}
formatCreateLabel={formatCreateLabel}
options={groupOptions}
placeholder="Choose"
onChange={onChange}
onChange={this.onChange}
/>
);
}
......
......@@ -11,6 +11,7 @@ import {
ColorPicker,
SeriesColorPickerPopoverWithTheme,
SecretFormField,
UnitPicker,
DataLinksEditor,
DataSourceHttpSettings,
} from '@grafana/ui';
......@@ -61,6 +62,11 @@ export function registerAngularDirectives() {
'onColorChange',
'onToggleAxis',
]);
react2AngularDirective('unitPicker', UnitPicker, [
'value',
'width',
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
react2AngularDirective('metricSelect', MetricSelect, [
'options',
'onChange',
......
import { getFlotTickDecimals } from 'app/core/utils/ticks';
import _ from 'lodash';
import { getValueFormat, ValueFormatter, stringToJsRegex, DecimalCount } from '@grafana/data';
import { getValueFormat, ValueFormatter, stringToJsRegex, DecimalCount, formattedValueToString } from '@grafana/data';
function matchSeriesOverride(aliasOrRegex: string, seriesAlias: string) {
if (!aliasOrRegex) {
......@@ -339,7 +339,7 @@ export default class TimeSeries {
if (!_.isFinite(value)) {
value = null; // Prevent NaN formatting
}
return this.valueFormater(value, this.decimals, this.scaledDecimals);
return formattedValueToString(this.valueFormater(value, this.decimals, this.scaledDecimals));
}
isMsResolutionNeeded() {
......
import kbn from './kbn';
import { DecimalCount, TimeZone } from '@grafana/data';
interface ValueFormatTest {
id: string;
decimals?: DecimalCount;
scaledDecimals?: DecimalCount;
timeZone?: TimeZone;
value: number;
result: string;
}
const formatTests: ValueFormatTest[] = [
// Currancy
{ id: 'currencyUSD', decimals: 2, value: 1532.82, result: '$1.53K' },
{ id: 'currencyKRW', decimals: 2, value: 1532.82, result: '₩1.53K' },
// Typical
{ id: 'ms', decimals: 4, value: 0.0024, result: '0.0024 ms' },
{ id: 'ms', decimals: 0, value: 100, result: '100 ms' },
{ id: 'ms', decimals: 2, value: 1250, result: '1.25 s' },
{ id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' },
{ id: 'ms', decimals: 0, value: 1200, result: '1 s' },
{ id: 'short', decimals: 0, scaledDecimals: -1, value: 98765, result: '98.77 K' },
{ id: 'short', decimals: 0, scaledDecimals: 0, value: 9876543, result: '9.876543 Mil' },
{ id: 'kbytes', decimals: 3, value: 10000000, result: '9.537 GiB' },
{ id: 'deckbytes', decimals: 3, value: 10000000, result: '10.000 GB' },
{ id: 'megwatt', decimals: 3, value: 1000, result: '1.000 GW' },
{ id: 'kohm', decimals: 3, value: 1000, result: '1.000 MΩ' },
{ id: 'Mohm', decimals: 3, value: 1000, result: '1.000 GΩ' },
{ id: 'farad', decimals: 3, value: 1000, result: '1.000 kF' },
{ id: 'µfarad', decimals: 3, value: 1000, result: '1.000 mF' },
{ id: 'nfarad', decimals: 3, value: 1000, result: '1.000 µF' },
{ id: 'pfarad', decimals: 3, value: 1000, result: '1.000 nF' },
{ id: 'ffarad', decimals: 3, value: 1000, result: '1.000 pF' },
{ id: 'henry', decimals: 3, value: 1000, result: '1.000 kH' },
{ id: 'mhenry', decimals: 3, value: 1000, result: '1.000 H' },
{ id: 'µhenry', decimals: 3, value: 1000, result: '1.000 mH' },
];
describe('Chcek KBN value formats', () => {
for (const test of formatTests) {
describe(`value format: ${test.id}`, () => {
it(`should translate ${test.value} as ${test.result}`, () => {
const result = kbn.valueFormats[test.id](test.value, test.decimals, test.scaledDecimals);
expect(result).toBe(test.result);
});
});
}
});
......@@ -6,6 +6,8 @@ import {
stringToJsRegex,
TimeRange,
deprecationWarning,
DecimalCount,
formattedValueToString,
} from '@grafana/data';
const kbn: any = {};
......@@ -308,7 +310,10 @@ if (typeof Proxy !== 'undefined') {
const formatter = getValueFormat(name);
if (formatter) {
return formatter;
// Return the results as a simple string
return (value: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount, isUtc?: boolean) => {
return formattedValueToString(formatter(value, decimals, scaledDecimals, isUtc));
};
}
// default to look here
......
......@@ -9,7 +9,7 @@
<div ng-if="yaxis.show">
<div class="gf-form">
<label class="gf-form-label width-6">Unit</label>
<div class="gf-form-dropdown-typeahead max-width-20" ng-model="yaxis.format" dropdown-typeahead2="ctrl.unitFormats" dropdown-typeahead-on-select="ctrl.setUnitFormat(yaxis, $subItem)"></div>
<unit-picker onChange="ctrl.setUnitFormat(yaxis)" value="yaxis.format" width="20" />
</div>
</div>
......
......@@ -49,9 +49,11 @@ export class AxesEditorCtrl {
}
}
setUnitFormat(axis: { format: any }, subItem: { value: any }) {
axis.format = subItem.value;
this.panelCtrl.render();
setUnitFormat(axis: { format: any }) {
return (unit: string) => {
axis.format = unit;
this.panelCtrl.render();
};
}
render() {
......
......@@ -35,6 +35,7 @@ import {
getDisplayProcessor,
getFlotPairsConstant,
PanelEvents,
formattedValueToString,
} from '@grafana/data';
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
......@@ -862,7 +863,7 @@ class GraphElement {
if (!formatter) {
throw new Error(`Unit '${format}' is not supported`);
}
return formatter(val, axis.tickDecimals, axis.scaledDecimals);
return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals));
};
}
......
......@@ -33,6 +33,7 @@ export const getGraphSeriesModel = (
const displayProcessor = getDisplayProcessor({
config: {
unit: fieldOptions?.defaults?.unit,
decimals: legendOptions.decimals,
},
});
......@@ -68,7 +69,6 @@ export const getGraphSeriesModel = (
return {
...statDisplayValue,
text: statDisplayValue.text,
title: stat,
};
});
......@@ -104,7 +104,7 @@ export const getGraphSeriesModel = (
type: timeField.type,
isUtc: timeZone === 'utc',
config: {
dateDisplayFormat: useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT,
unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
},
});
......
......@@ -6,6 +6,7 @@ import {
GrafanaThemeType,
stringToJsRegex,
ScopedVars,
formattedValueToString,
} from '@grafana/data';
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
import { TemplateSrv } from 'app/features/templating/template_srv';
......@@ -188,7 +189,7 @@ export class TableRenderer {
}
this.setColorState(v, column.style);
return valueFormatter(v, column.style.decimals, null);
return formattedValueToString(valueFormatter(v, column.style.decimals, null));
};
}
......@@ -226,7 +227,11 @@ export class TableRenderer {
}
formatColumnValue(colIndex: number, value: any) {
return this.formatters[colIndex] ? this.formatters[colIndex](value) : value;
const fmt = this.formatters[colIndex];
if (fmt) {
return fmt(value);
}
return value;
}
renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment