From d934b5465b62c8f84de4cd4fc45392b66652071e Mon Sep 17 00:00:00 2001 From: Anton Volnuhin Date: Sat, 31 Jan 2026 18:36:59 +0300 Subject: [PATCH] Add fixed subcategory order based on December 2025 data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 11 deletions(-) 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) {