Use consistent RUB formatting: integers with narrow no-break space

Add formatRUB() helper that formats amounts without decimals and uses
U+202F (narrow no-break space) as thousands separator and before ₽ symbol.
This prevents unwanted line breaks within currency amounts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Anton Volnuhin 2026-02-01 14:32:55 +03:00
parent 3e0726d34f
commit fbbf94c3c4

35
app.js
View File

@ -1,3 +1,10 @@
// Format RUB amount: no decimals, thin space (U+2009) as thousands separator
function formatRUB(amount) {
return Math.round(amount)
.toLocaleString('ru-RU')
.replace(/\u00a0/g, '\u202F');
}
// Initialize the chart // Initialize the chart
const chartDom = document.getElementById('chart-container'); const chartDom = document.getElementById('chart-container');
const myChart = echarts.init(chartDom); const myChart = echarts.init(chartDom);
@ -44,9 +51,9 @@ function navigateToHistoryState(state) {
option.series.data = state.data; option.series.data = state.data;
if (state.contextName) { if (state.contextName) {
option.graphic.elements[0].style.text = `${russianMonth}\n${state.contextName}\n${state.total.toFixed(0).toLocaleString()} `; option.graphic.elements[0].style.text = `${russianMonth}\n${state.contextName}\n${formatRUB(state.total)}\u202F`;
} else { } else {
option.graphic.elements[0].style.text = russianMonth + '\n' + state.total.toFixed(0).toLocaleString() + ' ₽'; option.graphic.elements[0].style.text = russianMonth + '\n' + formatRUB(state.total) + '\u202F₽';
} }
myChart.setOption(option, { replaceMerge: ['series'] }); myChart.setOption(option, { replaceMerge: ['series'] });
@ -687,13 +694,13 @@ function renderChart(data) {
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: function(info) { formatter: function(info) {
const value = info.value.toLocaleString(); const value = formatRUB(info.value);
const name = info.name; const name = info.name;
// Calculate percentage of total // Calculate percentage of total
const percentage = ((info.value / sunburstData.total) * 100).toFixed(1); const percentage = ((info.value / sunburstData.total) * 100).toFixed(1);
return `${name}<br/>Amount: ${value} RUB<br/>Percentage: ${percentage}%`; return `${name}<br/>Amount: ${value}\u202F₽<br/>Percentage: ${percentage}%`;
} }
} }
}, },
@ -705,7 +712,7 @@ function renderChart(data) {
top: 'middle', top: 'middle',
z: 100, z: 100,
style: { style: {
text: russianMonth + '\n' + sunburstData.total.toFixed(0).toLocaleString() + ' ₽', text: russianMonth + '\n' + formatRUB(sunburstData.total) + '\u202F₽',
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 18, fontSize: 18,
@ -907,7 +914,7 @@ function renderChart(data) {
// Update the center text to show the drilled-down category // Update the center text to show the drilled-down category
const russianMonth = getRussianMonthName(document.getElementById('month-select').value); const russianMonth = getRussianMonthName(document.getElementById('month-select').value);
option.graphic.elements[0].style.text = `${russianMonth}\n${params.name}\n${params.value.toFixed(0).toLocaleString()} `; option.graphic.elements[0].style.text = `${russianMonth}\n${params.name}\n${formatRUB(params.value)}\u202F`;
myChart.setOption(option, { replaceMerge: ['series'] }); myChart.setOption(option, { replaceMerge: ['series'] });
@ -1679,9 +1686,9 @@ async function selectMonth(index) {
// Update the total amount in the center text // Update the total amount in the center text
const russianMonth = getRussianMonthName(month); const russianMonth = getRussianMonthName(month);
if (targetName) { if (targetName) {
option.graphic.elements[0].style.text = `${russianMonth}\n${targetName}\n${targetTotal.toFixed(0).toLocaleString()} `; option.graphic.elements[0].style.text = `${russianMonth}\n${targetName}\n${formatRUB(targetTotal)}\u202F`;
} else { } else {
option.graphic.elements[0].style.text = russianMonth + '\n' + targetTotal.toFixed(0).toLocaleString() + ' ₽'; option.graphic.elements[0].style.text = russianMonth + '\n' + formatRUB(targetTotal) + '\u202F₽';
} }
myChart.setOption({ myChart.setOption({
@ -2098,7 +2105,7 @@ function setupHoverEvents(sunburstData, contextName = null) {
const amountSpan = document.createElement('span'); const amountSpan = document.createElement('span');
amountSpan.className = 'top-item-amount'; amountSpan.className = 'top-item-amount';
amountSpan.textContent = item.value.toLocaleString() + ' ₽'; amountSpan.textContent = formatRUB(item.value) + '\u202F₽';
itemDiv.appendChild(amountSpan); itemDiv.appendChild(amountSpan);
topItemsElement.appendChild(itemDiv); topItemsElement.appendChild(itemDiv);
}); });
@ -2125,7 +2132,7 @@ function setupHoverEvents(sunburstData, contextName = null) {
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
</svg> </svg>
</button> </button>
<span class="hover-amount">${value.toLocaleString()} </span> <span class="hover-amount">${formatRUB(value)}\u202F</span>
`; `;
// Add click handler to header eye button // Add click handler to header eye button
const headerEyeBtn = detailsHeader.querySelector('.header-eye-btn'); const headerEyeBtn = detailsHeader.querySelector('.header-eye-btn');
@ -2166,7 +2173,7 @@ function setupHoverEvents(sunburstData, contextName = null) {
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
</svg> </svg>
</button> </button>
<span class="hover-amount">${sunburstData.total.toLocaleString()} </span> <span class="hover-amount">${formatRUB(sunburstData.total)}\u202F</span>
`; `;
// Add click handler to header eye button // Add click handler to header eye button
@ -2518,7 +2525,7 @@ function renderTransactionTable() {
let value = transaction.originalRow[col]; let value = transaction.originalRow[col];
if ((col === 'amount_rub' || col === 'amount_original') && typeof value === 'number') { if ((col === 'amount_rub' || col === 'amount_original') && typeof value === 'number') {
value = value.toLocaleString(); value = formatRUB(value);
} }
td.textContent = value !== undefined && value !== null ? value : ''; td.textContent = value !== undefined && value !== null ? value : '';