Форматирование чисел под локаль

При построении диаграмм числовые значения отображаются в разных элементах интерфейса:

  • на осях;
  • во всплывающих подсказках;
  • в подписях данных;
  • в легендах;
  • в пользовательских callback-функциях.

По умолчанию JavaScript использует стандартное строковое представление чисел, которое не учитывает региональные особенности. Например:

1234567.89

Для русской локали чаще используется формат:

1 234 567,89

Для английской:

1,234,567.89

Корректное форматирование особенно важно:

  • в финансовых графиках;
  • в аналитических панелях;
  • в BI-системах;
  • в международных проектах;
  • при отображении валют и процентов.

Intl.NumberFormat

Основной инструмент локализации чисел в JavaScript — объект Intl.NumberFormat.

Пример:

const formatter = new Intl.NumberFormat('ru-RU');

console.log(formatter.format(1234567.89));

Результат:

1 234 567,89

Для английской локали:

const formatter = new Intl.NumberFormat('en-US');

console.log(formatter.format(1234567.89));

Результат:

1,234,567.89

Форматирование подписей оси Y

Наиболее частая задача — локализация подписей осей.

Базовый пример

<canvas id="chart"></canvas>
const ctx = document.getElementById('chart');

const formatter = new Intl.NumberFormat('ru-RU');

new Chart(ctx, {
    type: 'bar',

    data: {
        labels: ['Январь', 'Февраль', 'Март'],
        datasets: [{
            label: 'Продажи',
            data: [12500, 568000, 1450000]
        }]
    },

    options: {
        scales: {
            y: {
                ticks: {
                    callback(value) {
                        return formatter.format(value);
                    }
                }
            }
        }
    }
});

Результат:

12 500
568 000
1 450 000

Использование locale напрямую в Chart.js

Начиная с современных версий Chart.js появилась поддержка свойства locale.

options: {
    locale: 'ru-RU'
}

Полный пример:

new Chart(ctx, {
    type: 'line',

    data: {
        labels: ['Q1', 'Q2', 'Q3'],
        datasets: [{
            data: [1200, 3500, 8100]
        }]
    },

    options: {
        locale: 'ru-RU'
    }
});

Chart.js начинает использовать локализованное форматирование там, где это поддерживается внутренними механизмами библиотеки.

Однако callback-функции всё равно остаются наиболее гибким способом настройки.


Форматирование валют

Российский рубль

const rubFormatter = new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB'
});

Пример:

ticks: {
    callback(value) {
        return rubFormatter.format(value);
    }
}

Результат:

1 250,00 ₽
25 000,00 ₽

Форматирование долларов

const usdFormatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
});

Результат:

$1,500.00
$25,000.00

Форматирование процентов

Базовый вариант

const percentFormatter = new Intl.NumberFormat('ru-RU', {
    style: 'percent',
    minimumFractionDigits: 1
});
console.log(percentFormatter.format(0.256));

Результат:

25,6 %

Проценты в tooltip

const percentFormatter = new Intl.NumberFormat('ru-RU', {
    style: 'percent',
    minimumFractionDigits: 2
});

new Chart(ctx, {
    type: 'pie',

    data: {
        labels: ['Desktop', 'Mobile'],
        datasets: [{
            data: [0.73, 0.27]
        }]
    },

    options: {
        plugins: {
            tooltip: {
                callbacks: {
                    label(context) {
                        return percentFormatter.format(context.raw);
                    }
                }
            }
        }
    }
});

Форматирование больших чисел

Часто требуется сокращённое отображение:

1K
15M
2B

Для этого используется параметр notation.

Compact notation

const compactFormatter = new Intl.NumberFormat('en-US', {
    notation: 'compact'
});

Пример:

console.log(compactFormatter.format(1200));
console.log(compactFormatter.format(2500000));

Результат:

1.2K
2.5M

Компактное отображение для русской локали

const formatter = new Intl.NumberFormat('ru-RU', {
    notation: 'compact'
});

Результат:

1,2 тыс.
2,5 млн

Форматирование tooltip

Tooltip — один из главных элементов визуализации данных.

Полная настройка

const formatter = new Intl.NumberFormat('ru-RU');

new Chart(ctx, {
    type: 'line',

    data: {
        labels: ['Пн', 'Вт', 'Ср'],
        datasets: [{
            label: 'Посещения',
            data: [12500, 48200, 91800]
        }]
    },

    options: {
        plugins: {
            tooltip: {
                callbacks: {
                    label(context) {
                        const label = context.dataset.label;
                        const value = formatter.format(context.raw);

                        return `${label}: ${value}`;
                    }
                }
            }
        }
    }
});

Форматирование нескольких наборов данных

const rubFormatter = new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB'
});

const percentFormatter = new Intl.NumberFormat('ru-RU', {
    style: 'percent'
});
tooltip: {
    callbacks: {
        label(context) {

            if (context.dataset.label === 'Доход') {
                return rubFormatter.format(context.raw);
            }

            if (context.dataset.label === 'Конверсия') {
                return percentFormatter.format(context.raw);
            }

            return context.raw;
        }
    }
}

Форматирование подписей данных через chartjs-plugin-datalabels

Плагин:

chartjs-plugin-datalabels

Пример:

plugins: {
    datalabels: {
        formatter(value) {
            return new Intl.NumberFormat('ru-RU').format(value);
        }
    }
}

Управление количеством знаков после запятой

minimumFractionDigits

const formatter = new Intl.NumberFormat('ru-RU', {
    minimumFractionDigits: 2
});

Результат:

1 250,00

maximumFractionDigits

const formatter = new Intl.NumberFormat('ru-RU', {
    maximumFractionDigits: 1
});

Результат:

1250,5

Одновременное использование minimumFractionDigits и maximumFractionDigits

const formatter = new Intl.NumberFormat('ru-RU', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
});

Фиксированный вывод:

1250,00

Форматирование отрицательных чисел

const formatter = new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB'
});

console.log(formatter.format(-12500));

Результат:

-12 500,00 ₽

Форматирование научных данных

Для инженерных и научных графиков применяется scientific notation.

const formatter = new Intl.NumberFormat('en-US', {
    notation: 'scientific'
});

Пример:

console.log(formatter.format(1230000));

Результат:

1.23E6

Форматирование через arrow function

Короткая запись

ticks: {
    callback: value =>
        new Intl.NumberFormat('ru-RU').format(value)
}

Повторное использование formatter

Нежелательно создавать Intl.NumberFormat внутри callback при каждом вызове.

Плохой вариант:

callback(value) {
    return new Intl.NumberFormat('ru-RU').format(value);
}

Правильный подход:

const formatter = new Intl.NumberFormat('ru-RU');

callback(value) {
    return formatter.format(value);
}

Причины:

  • меньше нагрузка на GC;
  • меньше создание объектов;
  • выше производительность;
  • лучше читаемость.

Динамическая смена локали

Переключение языка интерфейса

function createFormatter(locale) {
    return new Intl.NumberFormat(locale);
}
let currentLocale = 'ru-RU';

let formatter = createFormatter(currentLocale);

Обновление диаграммы

function changeLocale(locale) {

    formatter = createFormatter(locale);

    chart.options.scales.y.ticks.callback = value =>
        formatter.format(value);

    chart.update();
}

Автоматическое определение локали браузера

const locale = navigator.language;

Пример:

const formatter = new Intl.NumberFormat(navigator.language);

Использование массива локалей

const formatter = new Intl.NumberFormat([
    'ru-RU',
    'en-US'
]);

JavaScript последовательно проверяет доступные локали.


Форматирование чисел в stacked chart

new Chart(ctx, {
    type: 'bar',

    data: {
        labels: ['Q1', 'Q2'],
        datasets: [
            {
                label: 'Доход',
                data: [250000, 320000]
            },
            {
                label: 'Расход',
                data: [180000, 210000]
            }
        ]
    },

    options: {
        scales: {
            x: {
                stacked: true
            },

            y: {
                stacked: true,

                ticks: {
                    callback(value) {
                        return new Intl.NumberFormat('ru-RU', {
                            notation: 'compact'
                        }).format(value);
                    }
                }
            }
        }
    }
});

Форматирование времени и чисел одновременно

В финансовых и аналитических графиках часто используются даты и локализованные значения.

tooltip: {
    callbacks: {

        title(items) {
            return new Date(items[0].parsed.x)
                .toLocaleDateString('ru-RU');
        },

        label(context) {

            return new Intl.NumberFormat('ru-RU', {
                style: 'currency',
                currency: 'RUB'
            }).format(context.raw.y);
        }
    }
}

Пользовательская функция форматирования

Универсальный formatter

function formatNumber(value, locale = 'ru-RU') {

    return new Intl.NumberFormat(locale, {
        maximumFractionDigits: 2
    }).format(value);
}

Использование:

ticks: {
    callback: value => formatNumber(value)
}

Форматирование миллионов и миллиардов вручную

Иногда стандартного compact notation недостаточно.

function abbreviateNumber(value) {

    if (value >= 1_000_000_000) {
        return (value / 1_000_000_000).toFixed(1) + 'B';
    }

    if (value >= 1_000_000) {
        return (value / 1_000_000).toFixed(1) + 'M';
    }

    if (value >= 1_000) {
        return (value / 1_000).toFixed(1) + 'K';
    }

    return value.toString();
}

Локализация чисел в doughnut chart

new Chart(ctx, {
    type: 'doughnut',

    data: {
        labels: ['Chrome', 'Firefox', 'Safari'],
        datasets: [{
            data: [65.2, 18.5, 11.7]
        }]
    },

    options: {
        plugins: {
            tooltip: {
                callbacks: {
                    label(context) {

                        return new Intl.NumberFormat('ru-RU', {
                            minimumFractionDigits: 1,
                            maximumFractionDigits: 1
                        }).format(context.raw) + '%';
                    }
                }
            }
        }
    }
});

Локализация чисел в radar chart

new Chart(ctx, {
    type: 'radar',

    data: {
        labels: ['CPU', 'RAM', 'Disk'],
        datasets: [{
            data: [12500, 8300, 15400]
        }]
    },

    options: {
        scales: {
            r: {
                ticks: {
                    callback(value) {
                        return new Intl.NumberFormat('ru-RU')
                            .format(value);
                    }
                }
            }
        }
    }
});

Типичные ошибки

Создание formatter внутри callback

Проблема:

callback(value) {
    return new Intl.NumberFormat('ru-RU').format(value);
}

Недостатки:

  • лишнее создание объектов;
  • ухудшение производительности;
  • увеличение нагрузки на рендеринг.

Неправильное форматирование процентов

Ошибка:

0.25%

Правильный подход:

new Intl.NumberFormat('ru-RU', {
    style: 'percent'
}).format(0.25);

Результат:

25 %

Смешивание строк и чисел

Плохо:

return value + ' ₽';

Лучше:

return formatter.format(value);

Преимущества:

  • корректные разделители;
  • правильное округление;
  • поддержка локалей;
  • корректное отображение валюты.

Практический пример финансового графика

const currencyFormatter = new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB',
    maximumFractionDigits: 0
});

const chart = new Chart(ctx, {

    type: 'line',

    data: {
        labels: [
            'Янв',
            'Фев',
            'Мар',
            'Апр'
        ],

        datasets: [{
            label: 'Прибыль',
            data: [
                1250000,
                1850000,
                2640000,
                3120000
            ]
        }]
    },

    options: {

        locale: 'ru-RU',

        scales: {
            y: {
                ticks: {
                    callback(value) {
                        return currencyFormatter
                            .format(value);
                    }
                }
            }
        },

        plugins: {
            tooltip: {
                callbacks: {
                    label(context) {

                        return currencyFormatter
                            .format(context.raw);
                    }
                }
            }
        }
    }
});