Dashboard: Formatting Currency - add new custom 'financial' currency format without abbreviations (#106604)

* Dashboard: ValueFormat - add custom 'financial' currency format without abbreviations

- Add fullCurrency formatter function that displays complete numeric values
- Apply performance suggestions from old PR
- Add unit tests
- Update documentation

* remove unnecesary `true` format support
This commit is contained in:
Alexa Vargas 2025-06-12 15:28:10 +02:00 committed by GitHub
parent e33ef2885f
commit d041ae8e73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 133 additions and 66 deletions

View File

@ -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:<suffix>` | Custom unit that should go after value. |
| `prefix:<prefix>` | Custom unit that should go before value. |
| `time:<format>` | 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:<base scale><unit characters>` | 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:<unit>` | Custom count unit. |
| `currency:<unit>` | Custom currency unit. |
| Custom unit | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `suffix:<suffix>` | Custom unit that should go after value. |
| `prefix:<prefix>` | Custom unit that should go before value. |
| `time:<format>` | 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:<base scale><unit characters>` | 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:<unit>` | Custom count unit. |
| `currency:<unit>` | Custom currency unit. |
| `currency:financial:<unit>` | 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:

View File

@ -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<number, Intl.NumberFormat>();
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('');

View File

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

View File

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