Fix consistent subcategory colors and mini-chart sync
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
b0983a751e
commit
e8318166dd
101
app.js
101
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 {
|
||||
|
||||
22
styles.css
22
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user