diff --git a/docs/sources/panels-visualizations/configure-standard-options/index.md b/docs/sources/panels-visualizations/configure-standard-options/index.md index fdad3c786f4..5b42b26f9cf 100644 --- a/docs/sources/panels-visualizations/configure-standard-options/index.md +++ b/docs/sources/panels-visualizations/configure-standard-options/index.md @@ -172,14 +172,15 @@ You can further define a custom unit with specific syntax. For example, to set a The following table lists the special syntax options for custom units: -| Custom unit | Description | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `suffix:` | Custom unit that should go after value. | -| `prefix:` | Custom unit that should go before value. | -| `time:` | Custom date time formats type, such as `time:YYYY-MM-DD`. Refer to [formats](https://momentjs.com/docs/#/displaying/) for the format syntax and options. | -| `si:` | Custom SI units, such as `si: mF`. You can specify both a unit and the source data scale. For example, if your source data is represented as milli-something, prefix the unit with the `m` SI scale character. | -| `count:` | Custom count unit. | -| `currency:` | Custom currency unit. | +| Custom unit | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `suffix:` | Custom unit that should go after value. | +| `prefix:` | Custom unit that should go before value. | +| `time:` | Custom date time formats type, such as `time:YYYY-MM-DD`. Refer to [formats](https://momentjs.com/docs/#/displaying/) for the format syntax and options. | +| `si:` | Custom SI units, such as `si: mF`. You can specify both a unit and the source data scale. For example, if your source data is represented as milli-something, prefix the unit with the `m` SI scale character. | +| `count:` | Custom count unit. | +| `currency:` | Custom currency unit. | +| `currency:financial:` | Full format currency unit without abbreviations. Displays complete numeric values instead of scaled abbreviations (K: Thousand, M: Million, B: Billion, T: Trillion). For example, `currency:financial:$` displays `500,555` instead of `$501K`. Add `:suffix` to place the symbol after the number: `currency:financial:€:suffix` displays `500,555€`. | You can also paste a native emoji in the **Unit** drop-down and select it as a custom unit: diff --git a/packages/grafana-data/src/valueFormats/symbolFormatters.ts b/packages/grafana-data/src/valueFormats/symbolFormatters.ts index d9cdc85616c..7f68a006805 100644 --- a/packages/grafana-data/src/valueFormats/symbolFormatters.ts +++ b/packages/grafana-data/src/valueFormats/symbolFormatters.ts @@ -26,6 +26,52 @@ export function currency(symbol: string, asSuffix?: boolean): ValueFormatter { }; } +/** + * Formats currency values without scaling abbreviations(K: Thousands, M: Millions, B: Billions), displaying full numeric values. + * Uses cached Intl.NumberFormat objects for performance. + * + * @param symbol - Currency symbol (e.g., '$', '€', '£') + * @param asSuffix - If true, places symbol after number + * + * @example + * fullCurrency('$')(1234.56, 2) // { prefix: '$', text: '1,234.56' } - forces 2 decimals + * fullCurrency('€', true)(42.5) // { suffix: '€', text: '42.5' } + */ +export function fullCurrency(symbol: string, asSuffix?: boolean): ValueFormatter { + const locale = Intl.NumberFormat().resolvedOptions().locale; + const defaultFormatter = new Intl.NumberFormat(locale, { minimumFractionDigits: 0, maximumFractionDigits: 1 }); + const formattersCache = new Map(); + + return (value: number | null, decimals?: DecimalCount) => { + if (value === null) { + return { text: '' }; + } + + const numericValue: number = value; + + let text: string; + if (decimals !== undefined && decimals !== null) { + let formatter = formattersCache.get(decimals); + if (!formatter) { + formatter = new Intl.NumberFormat(locale, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + formattersCache.set(decimals, formatter); + } + text = formatter.format(numericValue); + } else { + text = defaultFormatter.format(numericValue); + } + + return { + prefix: asSuffix ? '' : symbol, + suffix: asSuffix ? symbol : '', + text, + }; + }; +} + const SI_PREFIXES = ['f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; const SI_BASE_INDEX = SI_PREFIXES.indexOf(''); diff --git a/packages/grafana-data/src/valueFormats/valueFormats.test.ts b/packages/grafana-data/src/valueFormats/valueFormats.test.ts index a0df43a7e8a..c5eccfa4765 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.test.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.test.ts @@ -15,62 +15,67 @@ interface ValueFormatTest { describe('valueFormats', () => { it.each` - format | decimals | value | expected - ${'currencyUSD'} | ${2} | ${1532.82} | ${'$1.53K'} - ${'currencyKRW'} | ${2} | ${1532.82} | ${'₩1.53K'} - ${'currencyIDR'} | ${2} | ${1532.82} | ${'Rp1.53K'} - ${'none'} | ${undefined} | ${3.23} | ${'3.23'} - ${'none'} | ${undefined} | ${0.0245} | ${'0.0245'} - ${'none'} | ${undefined} | ${1 / 3} | ${'0.333'} - ${'ms'} | ${4} | ${0.0024} | ${'0.0024 ms'} - ${'ms'} | ${0} | ${100} | ${'100 ms'} - ${'ms'} | ${2} | ${1250} | ${'1.25 s'} - ${'ms'} | ${1} | ${10000086.123} | ${'2.8 hours'} - ${'ms'} | ${1} | ${-10000086.123} | ${'-2.8 hours'} - ${'ms'} | ${undefined} | ${1000} | ${'1 s'} - ${'ms'} | ${0} | ${1200} | ${'1 s'} - ${'short'} | ${undefined} | ${1000} | ${'1 K'} - ${'short'} | ${undefined} | ${1200} | ${'1.20 K'} - ${'short'} | ${undefined} | ${1250} | ${'1.25 K'} - ${'short'} | ${undefined} | ${1000000} | ${'1 Mil'} - ${'short'} | ${undefined} | ${1500000} | ${'1.50 Mil'} - ${'short'} | ${undefined} | ${1000120} | ${'1.00 Mil'} - ${'short'} | ${undefined} | ${98765} | ${'98.8 K'} - ${'short'} | ${undefined} | ${9876543} | ${'9.88 Mil'} - ${'short'} | ${undefined} | ${9876543} | ${'9.88 Mil'} - ${'kbytes'} | ${undefined} | ${10000000} | ${'9.54 GiB'} - ${'deckbytes'} | ${undefined} | ${10000000} | ${'10 GB'} - ${'megwatt'} | ${3} | ${1000} | ${'1.000 GW'} - ${'mohm'} | ${3} | ${1000} | ${'1.000 Ω'} - ${'kohm'} | ${3} | ${1000} | ${'1.000 MΩ'} - ${'Mohm'} | ${3} | ${1000} | ${'1.000 GΩ'} - ${'farad'} | ${3} | ${1000} | ${'1.000 kF'} - ${'µfarad'} | ${3} | ${1000} | ${'1.000 mF'} - ${'nfarad'} | ${3} | ${1000} | ${'1.000 µF'} - ${'pfarad'} | ${3} | ${1000} | ${'1.000 nF'} - ${'ffarad'} | ${3} | ${1000} | ${'1.000 pF'} - ${'henry'} | ${3} | ${1000} | ${'1.000 kH'} - ${'mhenry'} | ${3} | ${1000} | ${'1.000 H'} - ${'µhenry'} | ${3} | ${1000} | ${'1.000 mH'} - ${'a'} | ${0} | ${1532.82} | ${'1533 a'} - ${'b'} | ${0} | ${1532.82} | ${'1533 b'} - ${'prefix:b'} | ${undefined} | ${1532.82} | ${'b1533'} - ${'suffix:d'} | ${undefined} | ${1532.82} | ${'1533 d'} - ${'si:µF'} | ${2} | ${0} | ${'0.00 µF'} - ${'si:µF'} | ${2} | ${1234} | ${'1.23 mF'} - ${'si:µF'} | ${2} | ${1234000000} | ${'1.23 kF'} - ${'si:µF'} | ${2} | ${1234000000000000} | ${'1.23 GF'} - ${'count:xpm'} | ${2} | ${1234567} | ${'1.23M xpm'} - ${'count:x/min'} | ${2} | ${1234} | ${'1.23K x/min'} - ${'currency:@'} | ${2} | ${1234567} | ${'@1.23M'} - ${'currency:@'} | ${2} | ${1234} | ${'@1.23K'} - ${'time:YYYY'} | ${0} | ${dateTime(new Date(1999, 6, 2)).valueOf()} | ${'1999'} - ${'time:YYYY.MM'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010.07'} - ${'dateTimeAsIso'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'} - ${'dateTimeAsUS'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'07/02/2010 12:00:00 am'} - ${'dateTimeAsSystem'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'} - ${'dtdurationms'} | ${undefined} | ${100000} | ${'1 minute'} - ${'dtdurationms'} | ${undefined} | ${150000} | ${'2 minutes'} + format | decimals | value | expected + ${'currencyUSD'} | ${2} | ${1532.82} | ${'$1.53K'} + ${'currencyKRW'} | ${2} | ${1532.82} | ${'₩1.53K'} + ${'currencyIDR'} | ${2} | ${1532.82} | ${'Rp1.53K'} + ${'none'} | ${undefined} | ${3.23} | ${'3.23'} + ${'none'} | ${undefined} | ${0.0245} | ${'0.0245'} + ${'none'} | ${undefined} | ${1 / 3} | ${'0.333'} + ${'ms'} | ${4} | ${0.0024} | ${'0.0024 ms'} + ${'ms'} | ${0} | ${100} | ${'100 ms'} + ${'ms'} | ${2} | ${1250} | ${'1.25 s'} + ${'ms'} | ${1} | ${10000086.123} | ${'2.8 hours'} + ${'ms'} | ${1} | ${-10000086.123} | ${'-2.8 hours'} + ${'ms'} | ${undefined} | ${1000} | ${'1 s'} + ${'ms'} | ${0} | ${1200} | ${'1 s'} + ${'short'} | ${undefined} | ${1000} | ${'1 K'} + ${'short'} | ${undefined} | ${1200} | ${'1.20 K'} + ${'short'} | ${undefined} | ${1250} | ${'1.25 K'} + ${'short'} | ${undefined} | ${1000000} | ${'1 Mil'} + ${'short'} | ${undefined} | ${1500000} | ${'1.50 Mil'} + ${'short'} | ${undefined} | ${1000120} | ${'1.00 Mil'} + ${'short'} | ${undefined} | ${98765} | ${'98.8 K'} + ${'short'} | ${undefined} | ${9876543} | ${'9.88 Mil'} + ${'short'} | ${undefined} | ${9876543} | ${'9.88 Mil'} + ${'kbytes'} | ${undefined} | ${10000000} | ${'9.54 GiB'} + ${'deckbytes'} | ${undefined} | ${10000000} | ${'10 GB'} + ${'megwatt'} | ${3} | ${1000} | ${'1.000 GW'} + ${'mohm'} | ${3} | ${1000} | ${'1.000 Ω'} + ${'kohm'} | ${3} | ${1000} | ${'1.000 MΩ'} + ${'Mohm'} | ${3} | ${1000} | ${'1.000 GΩ'} + ${'farad'} | ${3} | ${1000} | ${'1.000 kF'} + ${'µfarad'} | ${3} | ${1000} | ${'1.000 mF'} + ${'nfarad'} | ${3} | ${1000} | ${'1.000 µF'} + ${'pfarad'} | ${3} | ${1000} | ${'1.000 nF'} + ${'ffarad'} | ${3} | ${1000} | ${'1.000 pF'} + ${'henry'} | ${3} | ${1000} | ${'1.000 kH'} + ${'mhenry'} | ${3} | ${1000} | ${'1.000 H'} + ${'µhenry'} | ${3} | ${1000} | ${'1.000 mH'} + ${'a'} | ${0} | ${1532.82} | ${'1533 a'} + ${'b'} | ${0} | ${1532.82} | ${'1533 b'} + ${'prefix:b'} | ${undefined} | ${1532.82} | ${'b1533'} + ${'suffix:d'} | ${undefined} | ${1532.82} | ${'1533 d'} + ${'si:µF'} | ${2} | ${0} | ${'0.00 µF'} + ${'si:µF'} | ${2} | ${1234} | ${'1.23 mF'} + ${'si:µF'} | ${2} | ${1234000000} | ${'1.23 kF'} + ${'si:µF'} | ${2} | ${1234000000000000} | ${'1.23 GF'} + ${'count:xpm'} | ${2} | ${1234567} | ${'1.23M xpm'} + ${'count:x/min'} | ${2} | ${1234} | ${'1.23K x/min'} + ${'currency:@'} | ${2} | ${1234567} | ${'@1.23M'} + ${'currency:@'} | ${2} | ${1234} | ${'@1.23K'} + ${'currency:financial:$'} | ${2} | ${1234567} | ${'$1,234,567.00'} + ${'currency:financial:$'} | ${0} | ${1234567} | ${'$1,234,567'} + ${'currency:financial:$'} | ${undefined} | ${1234567} | ${'$1,234,567'} + ${'currency:financial:€:suffix'} | ${2} | ${1234.56} | ${'1,234.56€'} + ${'currency:financial:COP:suffix'} | ${0} | ${500000} | ${'500,000COP'} + ${'time:YYYY'} | ${0} | ${dateTime(new Date(1999, 6, 2)).valueOf()} | ${'1999'} + ${'time:YYYY.MM'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010.07'} + ${'dateTimeAsIso'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'} + ${'dateTimeAsUS'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'07/02/2010 12:00:00 am'} + ${'dateTimeAsSystem'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'} + ${'dtdurationms'} | ${undefined} | ${100000} | ${'1 minute'} + ${'dtdurationms'} | ${undefined} | ${150000} | ${'2 minutes'} `( 'With format=$format decimals=$decimals and value=$value then result should be = $expected', async ({ format, value, decimals, expected }) => { diff --git a/packages/grafana-data/src/valueFormats/valueFormats.ts b/packages/grafana-data/src/valueFormats/valueFormats.ts index 0a4b943357f..a971c914c80 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.ts @@ -5,7 +5,7 @@ import { TimeZone } from '../types/time'; import { getCategories } from './categories'; import { toDateTimeValueFormatter } from './dateTimeFormatters'; -import { getOffsetFromSIPrefix, SIPrefix, currency } from './symbolFormatters'; +import { getOffsetFromSIPrefix, SIPrefix, currency, fullCurrency } from './symbolFormatters'; export interface FormattedValue { text: string; @@ -257,8 +257,23 @@ export function getValueFormat(id?: string | null): ValueFormatter { return simpleCountUnit(sub); } + // Supported formats: + // currency:$ -> scaled currency ($1.2K) + // currency:financial:$ -> full currency ($1,234) + // currency:financial:€:suffix -> full currency with suffix (1,234€) if (key === 'currency') { - return currency(sub); + const keySplit = sub.split(':'); + + if (keySplit[0] === 'financial' && keySplit.length >= 2) { + const symbol = keySplit[1]; + if (!symbol) { + return toFixedUnit(''); // fallback for empty symbol + } + const asSuffix = keySplit[2] === 'suffix'; + return fullCurrency(symbol, asSuffix); + } else { + return currency(sub); + } } if (key === 'bool') {