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
This commit is contained in:
Ryan McKinley 2019-12-04 00:08:07 -08:00 committed by Torkel Ödegaard
parent 3289ee8b77
commit d7c76dacad
40 changed files with 612 additions and 431 deletions

View File

@ -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');

View File

@ -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 };
};
}

View File

@ -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;
}

View File

@ -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>> {

View File

@ -1,20 +1,19 @@
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;

View File

@ -1,40 +1,41 @@
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');
});
});

View File

@ -1,43 +1,47 @@
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) };
}

View File

@ -1,4 +1,4 @@
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') },

View File

@ -9,6 +9,7 @@ import {
toDurationInSeconds,
toDurationInHoursMinutesSeconds,
} from './dateTimeFormatters';
import { formattedValueToString } from './valueFormats';
import { toUtc, dateTime } from '../datetime/moment_wrapper';
describe('date time formats', () => {
@ -19,226 +20,230 @@ describe('date time formats', () => {
it('should format as iso date', () => {
const expected = browserTime.format('YYYY-MM-DD HH:mm:ss');
const actual = dateTimeAsIso(epoch, 0, 0, false);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as iso date (in UTC)', () => {
const expected = utcTime.format('YYYY-MM-DD HH:mm:ss');
const actual = dateTimeAsIso(epoch, 0, 0, true);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as iso date and skip date when today', () => {
const now = dateTime();
const expected = now.format('HH:mm:ss');
const actual = dateTimeAsIso(now.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as iso date (in UTC) and skip date when today', () => {
const now = toUtc();
const expected = now.format('HH:mm:ss');
const actual = dateTimeAsIso(now.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as US date', () => {
const expected = browserTime.format('MM/DD/YYYY h:mm:ss a');
const actual = dateTimeAsUS(epoch, 0, 0, false);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as US date (in UTC)', () => {
const expected = utcTime.format('MM/DD/YYYY h:mm:ss a');
const actual = dateTimeAsUS(epoch, 0, 0, true);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as US date and skip date when today', () => {
const now = dateTime();
const expected = now.format('h:mm:ss a');
const actual = dateTimeAsUS(now.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as US date (in UTC) and skip date when today', () => {
const now = toUtc();
const expected = now.format('h:mm:ss a');
const actual = dateTimeAsUS(now.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as from now with days', () => {
const daysAgo = dateTime().add(-7, 'd');
const expected = '7 days ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as from now with days (in UTC)', () => {
const daysAgo = toUtc().add(-7, 'd');
const expected = '7 days ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as from now with minutes', () => {
const daysAgo = dateTime().add(-2, 'm');
const expected = '2 minutes ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
it('should format as from now with minutes (in UTC)', () => {
const daysAgo = toUtc().add(-2, 'm');
const expected = '2 minutes ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
expect(actual.text).toBe(expected);
});
});
describe('duration', () => {
it('0 milliseconds', () => {
const str = toDurationInMilliseconds(0, 0);
expect(str).toBe('0 milliseconds');
expect(formattedValueToString(str)).toBe('0 milliseconds');
});
it('1 millisecond', () => {
const str = toDurationInMilliseconds(1, 0);
expect(str).toBe('1 millisecond');
expect(formattedValueToString(str)).toBe('1 millisecond');
});
it('-1 millisecond', () => {
const str = toDurationInMilliseconds(-1, 0);
expect(str).toBe('1 millisecond ago');
expect(formattedValueToString(str)).toBe('1 millisecond ago');
});
it('seconds', () => {
const str = toDurationInSeconds(1, 0);
expect(str).toBe('1 second');
expect(formattedValueToString(str)).toBe('1 second');
});
it('minutes', () => {
const str = toDuration(1, 0, Interval.Minute);
expect(str).toBe('1 minute');
expect(formattedValueToString(str)).toBe('1 minute');
});
it('hours', () => {
const str = toDuration(1, 0, Interval.Hour);
expect(str).toBe('1 hour');
expect(formattedValueToString(str)).toBe('1 hour');
});
it('days', () => {
const str = toDuration(1, 0, Interval.Day);
expect(str).toBe('1 day');
expect(formattedValueToString(str)).toBe('1 day');
});
it('weeks', () => {
const str = toDuration(1, 0, Interval.Week);
expect(str).toBe('1 week');
expect(formattedValueToString(str)).toBe('1 week');
});
it('months', () => {
const str = toDuration(1, 0, Interval.Month);
expect(str).toBe('1 month');
expect(formattedValueToString(str)).toBe('1 month');
});
it('years', () => {
const str = toDuration(1, 0, Interval.Year);
expect(str).toBe('1 year');
expect(formattedValueToString(str)).toBe('1 year');
});
it('decimal days', () => {
const str = toDuration(1.5, 2, Interval.Day);
expect(str).toBe('1 day, 12 hours, 0 minutes');
expect(formattedValueToString(str)).toBe('1 day, 12 hours, 0 minutes');
});
it('decimal months', () => {
const str = toDuration(1.5, 3, Interval.Month);
expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
expect(formattedValueToString(str)).toBe('1 month, 2 weeks, 1 day, 0 hours');
});
it('no decimals', () => {
const str = toDuration(38898367008, 0, Interval.Millisecond);
expect(str).toBe('1 year');
expect(formattedValueToString(str)).toBe('1 year');
});
it('1 decimal', () => {
const str = toDuration(38898367008, 1, Interval.Millisecond);
expect(str).toBe('1 year, 2 months');
expect(formattedValueToString(str)).toBe('1 year, 2 months');
});
it('too many decimals', () => {
const str = toDuration(38898367008, 20, Interval.Millisecond);
expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
expect(formattedValueToString(str)).toBe(
'1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'
);
});
it('floating point error', () => {
const str = toDuration(36993906007, 8, Interval.Millisecond);
expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
expect(formattedValueToString(str)).toBe(
'1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'
);
});
it('1 dthms', () => {
const str = toDurationInHoursMinutesSeconds(1);
expect(str).toBe('00:00:01');
expect(formattedValueToString(str)).toBe('00:00:01');
});
it('-1 dthms', () => {
const str = toDurationInHoursMinutesSeconds(-1);
expect(str).toBe('00:00:01 ago');
expect(formattedValueToString(str)).toBe('00:00:01 ago');
});
it('0 dthms', () => {
const str = toDurationInHoursMinutesSeconds(0);
expect(str).toBe('00:00:00');
expect(formattedValueToString(str)).toBe('00:00:00');
});
});
describe('clock', () => {
it('size less than 1 second', () => {
const str = toClock(999, 0);
expect(str).toBe('999ms');
expect(formattedValueToString(str)).toBe('999ms');
});
describe('size less than 1 minute', () => {
it('default', () => {
const str = toClock(59999);
expect(str).toBe('59s:999ms');
expect(formattedValueToString(str)).toBe('59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(59999, 0);
expect(str).toBe('59s');
expect(formattedValueToString(str)).toBe('59s');
});
});
describe('size less than 1 hour', () => {
it('default', () => {
const str = toClock(3599999);
expect(str).toBe('59m:59s:999ms');
expect(formattedValueToString(str)).toBe('59m:59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(3599999, 0);
expect(str).toBe('59m');
expect(formattedValueToString(str)).toBe('59m');
});
it('decimals equals 1', () => {
const str = toClock(3599999, 1);
expect(str).toBe('59m:59s');
expect(formattedValueToString(str)).toBe('59m:59s');
});
});
describe('size greater than or equal 1 hour', () => {
it('default', () => {
const str = toClock(7199999);
expect(str).toBe('01h:59m:59s:999ms');
expect(formattedValueToString(str)).toBe('01h:59m:59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(7199999, 0);
expect(str).toBe('01h');
expect(formattedValueToString(str)).toBe('01h');
});
it('decimals equals 1', () => {
const str = toClock(7199999, 1);
expect(str).toBe('01h:59m');
expect(formattedValueToString(str)).toBe('01h:59m');
});
it('decimals equals 2', () => {
const str = toClock(7199999, 2);
expect(str).toBe('01h:59m:59s');
expect(formattedValueToString(str)).toBe('01h:59m:59s');
});
});
describe('size greater than or equal 1 day', () => {
it('default', () => {
const str = toClock(89999999);
expect(str).toBe('24h:59m:59s:999ms');
expect(formattedValueToString(str)).toBe('24h:59m:59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(89999999, 0);
expect(str).toBe('24h');
expect(formattedValueToString(str)).toBe('24h');
});
it('decimals equals 1', () => {
const str = toClock(89999999, 1);
expect(str).toBe('24h:59m');
expect(formattedValueToString(str)).toBe('24h:59m');
});
it('decimals equals 2', () => {
const str = toClock(89999999, 2);
expect(str).toBe('24h:59m:59s');
expect(formattedValueToString(str)).toBe('24h:59m:59s');
});
});
});

View File

@ -1,6 +1,6 @@
import { toDuration as duration, toUtc, dateTime } from '../datetime/moment_wrapper';
import { toFixed, toFixedScaled } from './valueFormats';
import { toFixed, toFixedScaled, FormattedValue, ValueFormatter } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
interface IntervalsInSeconds {
@ -29,13 +29,13 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
[Interval.Millisecond]: 0.001,
};
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (Math.abs(size) < 1000) {
return toFixed(size, decimals) + ' ns';
return { text: toFixed(size, decimals), suffix: ' ns' };
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs');
} else if (Math.abs(size) < 1000000000) {
@ -47,13 +47,13 @@ export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecim
}
}
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (Math.abs(size) < 1000) {
return toFixed(size, decimals) + ' µs';
return { text: toFixed(size, decimals), suffix: ' µs' };
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms');
} else {
@ -61,13 +61,13 @@ export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDeci
}
}
export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (Math.abs(size) < 1000) {
return toFixed(size, decimals) + ' ms';
return { text: toFixed(size, decimals), suffix: ' ms' };
} else if (Math.abs(size) < 60000) {
// Less than 1 min
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
@ -92,9 +92,9 @@ export function trySubstract(value1: DecimalCount, value2: DecimalCount): Decima
return undefined;
}
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
// Less than 1 µs, divide in ns
@ -111,7 +111,7 @@ export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?
}
if (Math.abs(size) < 60) {
return toFixed(size, decimals) + ' s';
return { text: toFixed(size, decimals), suffix: ' s' };
} else if (Math.abs(size) < 3600) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
@ -129,13 +129,13 @@ export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
}
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (Math.abs(size) < 60) {
return toFixed(size, decimals) + ' min';
return { text: toFixed(size, decimals), suffix: ' min' };
} else if (Math.abs(size) < 1440) {
return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour');
} else if (Math.abs(size) < 10080) {
@ -147,13 +147,13 @@ export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?
}
}
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (Math.abs(size) < 24) {
return toFixed(size, decimals) + ' hour';
return { text: toFixed(size, decimals), suffix: ' hour' };
} else if (Math.abs(size) < 168) {
return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day');
} else if (Math.abs(size) < 8760) {
@ -163,13 +163,13 @@ export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?:
}
}
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (Math.abs(size) < 7) {
return toFixed(size, decimals) + ' day';
return { text: toFixed(size, decimals), suffix: ' day' };
} else if (Math.abs(size) < 365) {
return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week');
} else {
@ -177,17 +177,22 @@ export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: D
}
}
export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string {
export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
if (size === 0) {
return '0 ' + timeScale + 's';
return { text: '0', suffix: ' ' + timeScale + 's' };
}
if (size < 0) {
return toDuration(-size, decimals, timeScale) + ' ago';
const v = toDuration(-size, decimals, timeScale);
if (!v.suffix) {
v.suffix = '';
}
v.suffix += ' ago';
return v;
}
const units = [
@ -228,17 +233,19 @@ export function toDuration(size: number, decimals: DecimalCount, timeScale: Inte
}
}
return strings.join(', ');
return { text: strings.join(', ') };
}
export function toClock(size: number, decimals?: DecimalCount) {
export function toClock(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return '';
return { text: '' };
}
// < 1 second
if (size < 1000) {
return toUtc(size).format('SSS\\m\\s');
return {
text: toUtc(size).format('SSS\\m\\s'),
};
}
// < 1 minute
@ -247,7 +254,7 @@ export function toClock(size: number, decimals?: DecimalCount) {
if (decimals === 0) {
format = 'ss\\s';
}
return toUtc(size).format(format);
return { text: toUtc(size).format(format) };
}
// < 1 hour
@ -258,7 +265,7 @@ export function toClock(size: number, decimals?: DecimalCount) {
} else if (decimals === 1) {
format = 'mm\\m:ss\\s';
}
return toUtc(size).format(format);
return { text: toUtc(size).format(format) };
}
let format = 'mm\\m:ss\\s:SSS\\m\\s';
@ -273,20 +280,26 @@ export function toClock(size: number, decimals?: DecimalCount) {
format = 'mm\\m:ss\\s';
}
return format ? `${hours}:${toUtc(size).format(format)}` : hours;
const text = format ? `${hours}:${toUtc(size).format(format)}` : hours;
return { text };
}
export function toDurationInMilliseconds(size: number, decimals: DecimalCount) {
export function toDurationInMilliseconds(size: number, decimals: DecimalCount): FormattedValue {
return toDuration(size, decimals, Interval.Millisecond);
}
export function toDurationInSeconds(size: number, decimals: DecimalCount) {
export function toDurationInSeconds(size: number, decimals: DecimalCount): FormattedValue {
return toDuration(size, decimals, Interval.Second);
}
export function toDurationInHoursMinutesSeconds(size: number): string {
export function toDurationInHoursMinutesSeconds(size: number): FormattedValue {
if (size < 0) {
return toDurationInHoursMinutesSeconds(-size) + ' ago';
const v = toDurationInHoursMinutesSeconds(-size);
if (!v.suffix) {
v.suffix = '';
}
v.suffix += ' ago';
return v;
}
const strings = [];
const numHours = Math.floor(size / 3600);
@ -295,40 +308,42 @@ export function toDurationInHoursMinutesSeconds(size: number): string {
numHours > 9 ? strings.push('' + numHours) : strings.push('0' + numHours);
numMinutes > 9 ? strings.push('' + numMinutes) : strings.push('0' + numMinutes);
numSeconds > 9 ? strings.push('' + numSeconds) : strings.push('0' + numSeconds);
return strings.join(':');
return { text: strings.join(':') };
}
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) {
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount): FormattedValue {
return toSeconds(size / 100, decimals, scaledDecimals);
}
export function toClockMilliseconds(size: number, decimals: DecimalCount) {
export function toClockMilliseconds(size: number, decimals: DecimalCount): FormattedValue {
return toClock(size, decimals);
}
export function toClockSeconds(size: number, decimals: DecimalCount) {
export function toClockSeconds(size: number, decimals: DecimalCount): FormattedValue {
return toClock(size * 1000, decimals);
}
export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? toUtc(value) : dateTime(value);
if (dateTime().isSame(value, 'day')) {
return time.format('HH:mm:ss');
}
return time.format('YYYY-MM-DD HH:mm:ss');
export function toDateTimeValueFormatter(pattern: string, todayPattern?: string): ValueFormatter {
return (value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean): FormattedValue => {
const time = isUtc ? toUtc(value) : dateTime(value);
if (todayPattern) {
if (dateTime().isSame(value, 'day')) {
return { text: time.format(todayPattern) };
}
}
return { text: time.format(pattern) };
};
}
export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? toUtc(value) : dateTime(value);
export const dateTimeAsIso = toDateTimeValueFormatter('YYYY-MM-DD HH:mm:ss', 'HH:mm:ss');
export const dateTimeAsUS = toDateTimeValueFormatter('MM/DD/YYYY h:mm:ss a', 'h:mm:ss a');
if (dateTime().isSame(value, 'day')) {
return time.format('h:mm:ss a');
}
return time.format('MM/DD/YYYY h:mm:ss a');
}
export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
export function dateTimeFromNow(
value: number,
decimals: DecimalCount,
scaledDecimals: DecimalCount,
isUtc?: boolean
): FormattedValue {
const time = isUtc ? toUtc(value) : dateTime(value);
return time.fromNow();
return { text: time.fromNow() };
}

View File

@ -1,10 +0,0 @@
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');
});
});

View File

@ -1,19 +1,59 @@
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 => {

View File

@ -1,6 +1,85 @@
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');
});
});
});

View File

@ -1,12 +1,24 @@
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: '' };
}
return toFixed(size, decimals) + ' ' + unit;
const text = toFixed(size, decimals);
if (unit) {
if (asPrefix) {
return { text, prefix: unit };
}
return { text, suffix: ' ' + 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 {

View File

@ -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 = {

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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>
`;

View File

@ -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,

View File

@ -0,0 +1,37 @@
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';

View File

@ -1,8 +1,6 @@
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 },
},

View File

@ -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>
);
})}

View File

@ -1,5 +1,5 @@
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

View File

@ -1,4 +1,4 @@
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,
});
}

View File

@ -1,7 +1,7 @@
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>
);
};

View File

@ -1,7 +1,7 @@
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;

View File

@ -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);

View File

@ -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 && (
<>

View File

@ -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>
);
};

View File

@ -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}
/>
);
}

View File

@ -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',

View File

@ -1,6 +1,6 @@
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() {

View File

@ -0,0 +1,52 @@
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);
});
});
}
});

View File

@ -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

View File

@ -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>

View File

@ -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() {

View File

@ -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));
};
}

View File

@ -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}`,
},
});

View File

@ -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) {