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
|
// Process each child
|
||||||
sortedChildren.forEach((child, i) => {
|
sortedChildren.forEach((child, i) => {
|
||||||
const color = colorPalette[i % colorPalette.length];
|
const color = getSubcategoryColor(params.name, child.name, i);
|
||||||
const newCategory = {
|
const newCategory = {
|
||||||
name: child.name,
|
name: child.name,
|
||||||
value: child.value,
|
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
|
// Generate conic-gradient CSS for a month's category breakdown
|
||||||
function generateMonthPreviewGradient(data) {
|
function generateMonthPreviewGradient(data) {
|
||||||
// Group by category and sum amounts
|
// 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
|
// Sort categories by predefined order
|
||||||
const sortedCategories = Object.keys(categoryTotals).sort((a, b) => {
|
const sortedCategories = Object.keys(categoryTotals).sort((a, b) => {
|
||||||
@ -1052,7 +1120,7 @@ function generateMonthPreviewGradient(data) {
|
|||||||
currentAngle += angle;
|
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
|
// 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
|
// Empty state - category doesn't exist
|
||||||
if (result.empty) {
|
if (result.empty) {
|
||||||
return 'conic-gradient(#e0e0e0 0deg 360deg)';
|
return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { transactions, level } = result;
|
const { transactions, level } = result;
|
||||||
@ -1151,15 +1219,14 @@ function generateDrilledDownGradient(monthData, path) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return 'conic-gradient(#e0e0e0 0deg 360deg)';
|
return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by value descending
|
// Sort by value descending
|
||||||
const sortedKeys = Object.keys(totals).sort((a, b) => totals[b] - totals[a]);
|
const sortedKeys = Object.keys(totals).sort((a, b) => totals[b] - totals[a]);
|
||||||
|
|
||||||
// Generate gradient with color variations
|
// Get the parent category for color lookup
|
||||||
const baseColor = getPathBaseColor(path);
|
const parentCategory = path.length > 0 ? path[0] : null;
|
||||||
const colors = generateColorGradient(baseColor, sortedKeys.length);
|
|
||||||
|
|
||||||
const gradientStops = [];
|
const gradientStops = [];
|
||||||
let currentAngle = 0;
|
let currentAngle = 0;
|
||||||
@ -1167,13 +1234,16 @@ function generateDrilledDownGradient(monthData, path) {
|
|||||||
sortedKeys.forEach((key, index) => {
|
sortedKeys.forEach((key, index) => {
|
||||||
const percentage = totals[key] / total;
|
const percentage = totals[key] / total;
|
||||||
const angle = percentage * 360;
|
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`);
|
gradientStops.push(`${color} ${currentAngle}deg ${currentAngle + angle}deg`);
|
||||||
currentAngle += angle;
|
currentAngle += angle;
|
||||||
});
|
});
|
||||||
|
|
||||||
return `conic-gradient(${gradientStops.join(', ')})`;
|
return `conic-gradient(from 90deg, ${gradientStops.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all month preview gradients based on current drill path
|
// 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)
|
// Transform children data for drill-down (extracted from click handler)
|
||||||
function transformDrillDownData(parentNode) {
|
function transformDrillDownData(parentNode, parentCategoryName) {
|
||||||
const colorPalette = [
|
|
||||||
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
|
|
||||||
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72'
|
|
||||||
];
|
|
||||||
|
|
||||||
const newData = [];
|
const newData = [];
|
||||||
const sortedChildren = [...parentNode.children].sort((a, b) => b.value - a.value);
|
const sortedChildren = [...parentNode.children].sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
sortedChildren.forEach((child, i) => {
|
sortedChildren.forEach((child, i) => {
|
||||||
const color = colorPalette[i % colorPalette.length];
|
const color = getSubcategoryColor(parentCategoryName, child.name, i);
|
||||||
const newCategory = {
|
const newCategory = {
|
||||||
name: child.name,
|
name: child.name,
|
||||||
value: child.value,
|
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)
|
// Transform this level's children to become top-level (same as click handler logic)
|
||||||
if (found.children && found.children.length > 0) {
|
if (found.children && found.children.length > 0) {
|
||||||
currentData = transformDrillDownData(found);
|
currentData = transformDrillDownData(found, targetName);
|
||||||
} else if (found.transactions && found.transactions.length > 0) {
|
} else if (found.transactions && found.transactions.length > 0) {
|
||||||
currentData = transformTransactionData(found);
|
currentData = transformTransactionData(found);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
22
styles.css
22
styles.css
@ -90,8 +90,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.month-btn.active {
|
.month-btn.active {
|
||||||
background-color: #5470c6;
|
background-color: #4a4a4a;
|
||||||
border-color: #5470c6;
|
border-color: #4a4a4a;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.month-btn.active .month-preview::after {
|
.month-btn.active .month-preview::after {
|
||||||
background: #5470c6;
|
background: #4a4a4a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-label {
|
.month-label {
|
||||||
@ -234,23 +234,33 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.top-item-name {
|
.top-item-name {
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-item-amount {
|
.top-item-amount {
|
||||||
margin-left: 15px;
|
margin-left: auto;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex-shrink: 0;
|
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 */
|
/* Add responsive design for smaller screens */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
#details-box {
|
#details-box {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user