diff --git a/app.js b/app.js index 2337ca8..90be9a2 100644 --- a/app.js +++ b/app.js @@ -725,7 +725,7 @@ function renderChart(data) { // Handle different cases based on the type of node clicked if (params.data.children && params.data.children.length > 0) { // 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 sortedChildren.forEach((child, i) => { @@ -1071,6 +1071,12 @@ const defaultColorPalette = [ '#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 function getSubcategoryColor(category, subcategory, index) { if (subcategoryColors[category] && subcategoryColors[category][subcategory]) { @@ -1079,6 +1085,30 @@ function getSubcategoryColor(category, subcategory, index) { 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 function generateMonthPreviewGradient(data) { // Group by category and sum amounts @@ -1222,12 +1252,25 @@ function generateDrilledDownGradient(monthData, path) { return 'conic-gradient(from 90deg, #e0e0e0 0deg 360deg)'; } - // Sort by value descending - const sortedKeys = Object.keys(totals).sort((a, b) => totals[b] - totals[a]); - - // Get the parent category for color lookup + // Get the parent category for color lookup and ordering 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 = []; let currentAngle = 0; @@ -1351,7 +1394,7 @@ async function loadAvailableMonths() { // Transform children data for drill-down (extracted from click handler) function transformDrillDownData(parentNode, parentCategoryName) { const newData = []; - const sortedChildren = [...parentNode.children].sort((a, b) => b.value - a.value); + const sortedChildren = sortBySubcategoryOrder(parentNode.children, parentCategoryName); sortedChildren.forEach((child, 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) - displayDetailsItems([...sunburstData.data].sort((a, b) => b.value - a.value), sunburstData.total); + // Show items - sort by value for root level, use fixed order for drilled-down + 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 @@ -2144,7 +2190,7 @@ function setupHoverEvents(sunburstData, contextName = null) { const MAX_DETAIL_ITEMS = 10; let itemsToShow = []; 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) { 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; let itemsToShow = []; 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) { 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; let itemsToShow = []; 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 displayedNames = new Set(); for (const child of sortedChildren) {