From e8318166dd5509841f2f48cbff8bcc6ff0873bb5 Mon Sep 17 00:00:00 2001 From: Anton Volnuhin Date: Sat, 31 Jan 2026 18:26:18 +0300 Subject: [PATCH] Fix consistent subcategory colors and mini-chart sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add fixed subcategory color mapping based on December 2025 data - Update click handler and transformDrillDownData to use getSubcategoryColor - Rotate mini-charts 90° clockwise to match main chart orientation - Change month selector active state from blue to neutral gray (#4a4a4a) - Ensure colors stay consistent when switching months while drilled down Co-Authored-By: Claude Opus 4.5 --- app.js | 101 +++++++++++++++++++++++++++++++++++++++++++---------- styles.css | 22 ++++++++---- 2 files changed, 99 insertions(+), 24 deletions(-) diff --git a/app.js b/app.js index ffe1f29..2337ca8 100644 --- a/app.js +++ b/app.js @@ -729,7 +729,7 @@ function renderChart(data) { // Process each child sortedChildren.forEach((child, i) => { - const color = colorPalette[i % colorPalette.length]; + const color = getSubcategoryColor(params.name, child.name, i); const newCategory = { name: child.name, value: child.value, @@ -1011,6 +1011,74 @@ const categoryOrder = [ 'Здоровье', 'Логистика', 'Расходники', 'Красота' ]; +// Fixed subcategory colors based on December 2025 data (sorted by value) +// This ensures consistent colors across all months +const subcategoryColors = { + 'Квартира': { + 'ЖКХ': '#5470c6', + 'Лев Петрович': '#91cc75' + }, + 'Еда': { + 'Продукты': '#5470c6', + 'Готовая': '#91cc75' + }, + 'Технологии': { + 'Инфраструктура домашнего интернета': '#5470c6', + 'Умный дом': '#91cc75', + 'AI': '#fac858', + 'Мобильная связь': '#ee6666' + }, + 'Развлечения': { + 'Подарки': '#5470c6', + 'Хобби Антона': '#91cc75', + 'Хобби Залины': '#fac858', + 'Подписки': '#ee6666', + 'Чтение': '#73c0de', + 'Работа Залины': '#3ba272', + 'Девайсы': '#fc8452' + }, + 'Семьи': { + 'Залина': '#5470c6', + 'Антон': '#91cc75' + }, + 'Здоровье': { + 'Терапия': '#5470c6', + 'Лекарства': '#91cc75', + 'Спортивное питание': '#fac858' + }, + 'Логистика': { + 'Такси': '#5470c6', + 'Доставка': '#91cc75', + 'Чаевые': '#fac858' + }, + 'Расходники': { + 'Замена разных фильтров': '#5470c6', + 'Санитарное': '#91cc75', + 'Батарейки': '#fac858', + 'Мелкий ремонт': '#ee6666', + 'Средства для посудомоек': '#73c0de' + }, + 'Красота': { + 'Одежда': '#5470c6', + 'Салоны красоты': '#91cc75', + 'Кремы': '#fac858' + } +}; + +// Color palette for items not in the predefined mapping +const defaultColorPalette = [ + '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', + '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72' +]; + +// Get color for a subcategory, using fixed mapping or fallback to index-based +function getSubcategoryColor(category, subcategory, index) { + if (subcategoryColors[category] && subcategoryColors[category][subcategory]) { + return subcategoryColors[category][subcategory]; + } + return defaultColorPalette[index % defaultColorPalette.length]; +} + // Generate conic-gradient CSS for a month's category breakdown function generateMonthPreviewGradient(data) { // Group by category and sum amounts @@ -1026,7 +1094,7 @@ function generateMonthPreviewGradient(data) { } }); - if (total === 0) return 'conic-gradient(#eee 0deg 360deg)'; + if (total === 0) return 'conic-gradient(from 90deg, #eee 0deg 360deg)'; // Sort categories by predefined order const sortedCategories = Object.keys(categoryTotals).sort((a, b) => { @@ -1052,7 +1120,7 @@ function generateMonthPreviewGradient(data) { currentAngle += angle; }); - return `conic-gradient(${gradientStops.join(', ')})`; + return `conic-gradient(from 90deg, ${gradientStops.join(', ')})`; } // Find category data at a given path in month's transaction data @@ -1128,7 +1196,7 @@ function generateDrilledDownGradient(monthData, path) { // Empty state - category doesn't exist if (result.empty) { - return 'conic-gradient(#e0e0e0 0deg 360deg)'; + return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)'; } const { transactions, level } = result; @@ -1151,15 +1219,14 @@ function generateDrilledDownGradient(monthData, path) { }); if (total === 0) { - return 'conic-gradient(#e0e0e0 0deg 360deg)'; + return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)'; } // Sort by value descending const sortedKeys = Object.keys(totals).sort((a, b) => totals[b] - totals[a]); - // Generate gradient with color variations - const baseColor = getPathBaseColor(path); - const colors = generateColorGradient(baseColor, sortedKeys.length); + // Get the parent category for color lookup + const parentCategory = path.length > 0 ? path[0] : null; const gradientStops = []; let currentAngle = 0; @@ -1167,13 +1234,16 @@ function generateDrilledDownGradient(monthData, path) { sortedKeys.forEach((key, index) => { const percentage = totals[key] / total; const angle = percentage * 360; - const color = colors[index] || categoryColors[index % categoryColors.length]; + // Use fixed subcategory colors for first level, fallback to palette for deeper levels + const color = (level === 'subcategory' && parentCategory) + ? getSubcategoryColor(parentCategory, key, index) + : defaultColorPalette[index % defaultColorPalette.length]; gradientStops.push(`${color} ${currentAngle}deg ${currentAngle + angle}deg`); currentAngle += angle; }); - return `conic-gradient(${gradientStops.join(', ')})`; + return `conic-gradient(from 90deg, ${gradientStops.join(', ')})`; } // Update all month preview gradients based on current drill path @@ -1279,17 +1349,12 @@ async function loadAvailableMonths() { } // Transform children data for drill-down (extracted from click handler) -function transformDrillDownData(parentNode) { - const colorPalette = [ - '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', - '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72' - ]; - +function transformDrillDownData(parentNode, parentCategoryName) { const newData = []; const sortedChildren = [...parentNode.children].sort((a, b) => b.value - a.value); sortedChildren.forEach((child, i) => { - const color = colorPalette[i % colorPalette.length]; + const color = getSubcategoryColor(parentCategoryName, child.name, i); const newCategory = { name: child.name, value: child.value, @@ -1454,7 +1519,7 @@ function navigateToPath(sunburstData, path) { // Transform this level's children to become top-level (same as click handler logic) if (found.children && found.children.length > 0) { - currentData = transformDrillDownData(found); + currentData = transformDrillDownData(found, targetName); } else if (found.transactions && found.transactions.length > 0) { currentData = transformTransactionData(found); } else { diff --git a/styles.css b/styles.css index 3f8bb2f..e43a85c 100644 --- a/styles.css +++ b/styles.css @@ -90,8 +90,8 @@ body { } .month-btn.active { - background-color: #5470c6; - border-color: #5470c6; + background-color: #4a4a4a; + border-color: #4a4a4a; color: white; } @@ -120,7 +120,7 @@ body { } .month-btn.active .month-preview::after { - background: #5470c6; + background: #4a4a4a; } .month-label { @@ -234,23 +234,33 @@ body { } .top-item-name { - flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; min-width: 0; + width: 50%; + flex-shrink: 0; } .top-item-amount { - margin-left: 15px; + margin-left: auto; font-weight: bold; - min-width: 120px; + min-width: 100px; text-align: right; flex-shrink: 0; } +.top-item-percent { + margin-left: 8px; + width: 45px; + text-align: right; + flex-shrink: 0; + color: #999; + font-weight: 400; +} + /* Add responsive design for smaller screens */ @media (max-width: 1200px) { #details-box {