Add fixed subcategory order based on December 2025 data
- Add subcategoryOrder derived from subcategoryColors - Add sortBySubcategoryOrder helper function - Update click handler, transformDrillDownData, and mini-chart gradient to use fixed order instead of value-based sorting - Update details box to preserve fixed order for drilled-down views while keeping value-based sorting for root level categories This ensures subcategories maintain consistent positions when switching months, even when their values differ (e.g., "Хобби Залины" always comes before "Подписки" regardless of which has higher spending that month). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8318166dd
commit
d934b5465b
68
app.js
68
app.js
@ -725,7 +725,7 @@ function renderChart(data) {
|
|||||||
// Handle different cases based on the type of node clicked
|
// Handle different cases based on the type of node clicked
|
||||||
if (params.data.children && params.data.children.length > 0) {
|
if (params.data.children && params.data.children.length > 0) {
|
||||||
// Case 1: Node has children (category or subcategory)
|
// Case 1: Node has children (category or subcategory)
|
||||||
const sortedChildren = [...params.data.children].sort((a, b) => b.value - a.value);
|
const sortedChildren = sortBySubcategoryOrder(params.data.children, params.name);
|
||||||
|
|
||||||
// Process each child
|
// Process each child
|
||||||
sortedChildren.forEach((child, i) => {
|
sortedChildren.forEach((child, i) => {
|
||||||
@ -1071,6 +1071,12 @@ const defaultColorPalette = [
|
|||||||
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72'
|
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Fixed subcategory order derived from subcategoryColors (December 2025 order)
|
||||||
|
const subcategoryOrder = {};
|
||||||
|
for (const category in subcategoryColors) {
|
||||||
|
subcategoryOrder[category] = Object.keys(subcategoryColors[category]);
|
||||||
|
}
|
||||||
|
|
||||||
// Get color for a subcategory, using fixed mapping or fallback to index-based
|
// Get color for a subcategory, using fixed mapping or fallback to index-based
|
||||||
function getSubcategoryColor(category, subcategory, index) {
|
function getSubcategoryColor(category, subcategory, index) {
|
||||||
if (subcategoryColors[category] && subcategoryColors[category][subcategory]) {
|
if (subcategoryColors[category] && subcategoryColors[category][subcategory]) {
|
||||||
@ -1079,6 +1085,30 @@ function getSubcategoryColor(category, subcategory, index) {
|
|||||||
return defaultColorPalette[index % defaultColorPalette.length];
|
return defaultColorPalette[index % defaultColorPalette.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort items by fixed subcategory order, with unknown items at the end sorted by value
|
||||||
|
function sortBySubcategoryOrder(items, parentCategory, nameGetter = (item) => item.name) {
|
||||||
|
const order = subcategoryOrder[parentCategory] || [];
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
const aName = nameGetter(a);
|
||||||
|
const bName = nameGetter(b);
|
||||||
|
const aIndex = order.indexOf(aName);
|
||||||
|
const bIndex = order.indexOf(bName);
|
||||||
|
|
||||||
|
// Both in predefined order - sort by that order
|
||||||
|
if (aIndex !== -1 && bIndex !== -1) {
|
||||||
|
return aIndex - bIndex;
|
||||||
|
}
|
||||||
|
// Only a is in predefined order - a comes first
|
||||||
|
if (aIndex !== -1) return -1;
|
||||||
|
// Only b is in predefined order - b comes first
|
||||||
|
if (bIndex !== -1) return 1;
|
||||||
|
// Neither in predefined order - sort by value descending
|
||||||
|
const aValue = typeof a === 'object' ? (a.value || 0) : 0;
|
||||||
|
const bValue = typeof b === 'object' ? (b.value || 0) : 0;
|
||||||
|
return bValue - aValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -1222,12 +1252,25 @@ function generateDrilledDownGradient(monthData, path) {
|
|||||||
return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)';
|
return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by value descending
|
// Get the parent category for color lookup and ordering
|
||||||
const sortedKeys = Object.keys(totals).sort((a, b) => totals[b] - totals[a]);
|
|
||||||
|
|
||||||
// Get the parent category for color lookup
|
|
||||||
const parentCategory = path.length > 0 ? path[0] : null;
|
const parentCategory = path.length > 0 ? path[0] : null;
|
||||||
|
|
||||||
|
// Sort by fixed subcategory order (for first level), fallback to value for unknown items
|
||||||
|
const order = subcategoryOrder[parentCategory] || [];
|
||||||
|
const sortedKeys = Object.keys(totals).sort((a, b) => {
|
||||||
|
const aIndex = order.indexOf(a);
|
||||||
|
const bIndex = order.indexOf(b);
|
||||||
|
|
||||||
|
// Both in predefined order - sort by that order
|
||||||
|
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||||
|
// Only a is in predefined order - a comes first
|
||||||
|
if (aIndex !== -1) return -1;
|
||||||
|
// Only b is in predefined order - b comes first
|
||||||
|
if (bIndex !== -1) return 1;
|
||||||
|
// Neither in predefined order - sort by value descending
|
||||||
|
return totals[b] - totals[a];
|
||||||
|
});
|
||||||
|
|
||||||
const gradientStops = [];
|
const gradientStops = [];
|
||||||
let currentAngle = 0;
|
let currentAngle = 0;
|
||||||
|
|
||||||
@ -1351,7 +1394,7 @@ 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, parentCategoryName) {
|
function transformDrillDownData(parentNode, parentCategoryName) {
|
||||||
const newData = [];
|
const newData = [];
|
||||||
const sortedChildren = [...parentNode.children].sort((a, b) => b.value - a.value);
|
const sortedChildren = sortBySubcategoryOrder(parentNode.children, parentCategoryName);
|
||||||
|
|
||||||
sortedChildren.forEach((child, i) => {
|
sortedChildren.forEach((child, i) => {
|
||||||
const color = getSubcategoryColor(parentCategoryName, child.name, i);
|
const color = getSubcategoryColor(parentCategoryName, child.name, i);
|
||||||
@ -2095,8 +2138,11 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show top categories as default items (sorted by value descending)
|
// Show items - sort by value for root level, use fixed order for drilled-down
|
||||||
displayDetailsItems([...sunburstData.data].sort((a, b) => b.value - a.value), sunburstData.total);
|
const itemsToDisplay = contextName
|
||||||
|
? [...sunburstData.data] // Drilled down: use fixed subcategory order
|
||||||
|
: [...sunburstData.data].sort((a, b) => b.value - a.value); // Root level: sort by value
|
||||||
|
displayDetailsItems(itemsToDisplay, sunburstData.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show default view initially
|
// Show default view initially
|
||||||
@ -2144,7 +2190,7 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
const MAX_DETAIL_ITEMS = 10;
|
const MAX_DETAIL_ITEMS = 10;
|
||||||
let itemsToShow = [];
|
let itemsToShow = [];
|
||||||
if (params.data.children && params.data.children.length > 0) {
|
if (params.data.children && params.data.children.length > 0) {
|
||||||
itemsToShow = [...params.data.children].sort((a, b) => b.value - a.value);
|
itemsToShow = [...params.data.children];
|
||||||
}
|
}
|
||||||
if (itemsToShow.length < MAX_DETAIL_ITEMS && params.data.transactions) {
|
if (itemsToShow.length < MAX_DETAIL_ITEMS && params.data.transactions) {
|
||||||
const currentColor = params.color || (params.data.itemStyle ? params.data.itemStyle.color : '#cccccc');
|
const currentColor = params.color || (params.data.itemStyle ? params.data.itemStyle.color : '#cccccc');
|
||||||
@ -2167,7 +2213,7 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
const MAX_DETAIL_ITEMS = 10;
|
const MAX_DETAIL_ITEMS = 10;
|
||||||
let itemsToShow = [];
|
let itemsToShow = [];
|
||||||
if (params.data.children && params.data.children.length > 0) {
|
if (params.data.children && params.data.children.length > 0) {
|
||||||
itemsToShow = [...params.data.children].sort((a, b) => b.value - a.value);
|
itemsToShow = [...params.data.children];
|
||||||
}
|
}
|
||||||
if (itemsToShow.length < MAX_DETAIL_ITEMS && params.data.transactions) {
|
if (itemsToShow.length < MAX_DETAIL_ITEMS && params.data.transactions) {
|
||||||
const currentColor = params.color || (params.data.itemStyle ? params.data.itemStyle.color : '#cccccc');
|
const currentColor = params.color || (params.data.itemStyle ? params.data.itemStyle.color : '#cccccc');
|
||||||
@ -2189,7 +2235,7 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
const MAX_DETAIL_ITEMS = 10;
|
const MAX_DETAIL_ITEMS = 10;
|
||||||
let itemsToShow = [];
|
let itemsToShow = [];
|
||||||
if (params.data.children && params.data.children.length > 0) {
|
if (params.data.children && params.data.children.length > 0) {
|
||||||
const sortedChildren = [...params.data.children].sort((a, b) => b.value - a.value);
|
const sortedChildren = [...params.data.children];
|
||||||
const allMicrocategories = [];
|
const allMicrocategories = [];
|
||||||
const displayedNames = new Set();
|
const displayedNames = new Set();
|
||||||
for (const child of sortedChildren) {
|
for (const child of sortedChildren) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user