feat: sync drill-down state across mini-charts and months
- Add currentDrillPath to track category hierarchy during drill-down - Mini-charts now update to show the same category breakdown when drilling into a category on the main chart - Switching months while drilled down preserves the category path, navigating to the same category in the new month - Empty state shown for months that don't have the drilled category - Browser back/forward navigation syncs mini-charts correctly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0bc2725d4d
commit
b0983a751e
425
app.js
425
app.js
@ -8,16 +8,20 @@ let showDefaultView = null; // Reference to reset details panel to default view
|
|||||||
// Drill-down history for back/forward navigation
|
// Drill-down history for back/forward navigation
|
||||||
let drillDownHistory = [];
|
let drillDownHistory = [];
|
||||||
let historyIndex = -1;
|
let historyIndex = -1;
|
||||||
|
let currentDrillPath = []; // Tracks drill-down hierarchy: ["Еда", "Рестораны", ...]
|
||||||
|
|
||||||
// Save current chart state to history
|
// Save current chart state to history
|
||||||
function saveToHistory(data, total, contextName, isInitial = false) {
|
function saveToHistory(data, total, contextName, isInitial = false, path = []) {
|
||||||
// Remove any forward history when drilling down from middle of history
|
// Remove any forward history when drilling down from middle of history
|
||||||
if (historyIndex < drillDownHistory.length - 1) {
|
if (historyIndex < drillDownHistory.length - 1) {
|
||||||
drillDownHistory = drillDownHistory.slice(0, historyIndex + 1);
|
drillDownHistory = drillDownHistory.slice(0, historyIndex + 1);
|
||||||
}
|
}
|
||||||
drillDownHistory.push({ data, total, contextName });
|
drillDownHistory.push({ data, total, contextName, path: [...path] });
|
||||||
historyIndex = drillDownHistory.length - 1;
|
historyIndex = drillDownHistory.length - 1;
|
||||||
|
|
||||||
|
// Update current drill path
|
||||||
|
currentDrillPath = [...path];
|
||||||
|
|
||||||
// Push to browser history (use replaceState for initial state)
|
// Push to browser history (use replaceState for initial state)
|
||||||
const state = { drillDown: historyIndex };
|
const state = { drillDown: historyIndex };
|
||||||
if (isInitial) {
|
if (isInitial) {
|
||||||
@ -31,6 +35,7 @@ function saveToHistory(data, total, contextName, isInitial = false) {
|
|||||||
function resetHistory() {
|
function resetHistory() {
|
||||||
drillDownHistory = [];
|
drillDownHistory = [];
|
||||||
historyIndex = -1;
|
historyIndex = -1;
|
||||||
|
currentDrillPath = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to a specific history state
|
// Navigate to a specific history state
|
||||||
@ -46,6 +51,10 @@ function navigateToHistoryState(state) {
|
|||||||
|
|
||||||
myChart.setOption(option, { replaceMerge: ['series'] });
|
myChart.setOption(option, { replaceMerge: ['series'] });
|
||||||
setupHoverEvents({ total: state.total, data: state.data }, state.contextName);
|
setupHoverEvents({ total: state.total, data: state.data }, state.contextName);
|
||||||
|
|
||||||
|
// Restore drill path and update mini-charts
|
||||||
|
currentDrillPath = state.path ? [...state.path] : [];
|
||||||
|
updateAllMonthPreviews();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go back in drill-down history
|
// Go back in drill-down history
|
||||||
@ -421,7 +430,7 @@ function renderChart(data) {
|
|||||||
|
|
||||||
// Reset and initialize history with the root state
|
// Reset and initialize history with the root state
|
||||||
resetHistory();
|
resetHistory();
|
||||||
saveToHistory(sunburstData.data, sunburstData.total, null, true);
|
saveToHistory(sunburstData.data, sunburstData.total, null, true, []);
|
||||||
|
|
||||||
// Get the currently selected month
|
// Get the currently selected month
|
||||||
const selectedMonth = document.getElementById('month-select').value;
|
const selectedMonth = document.getElementById('month-select').value;
|
||||||
@ -876,8 +885,9 @@ function renderChart(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new state to history
|
// Build the new path by appending the clicked category
|
||||||
saveToHistory(newData, params.value, params.name);
|
const newPath = [...currentDrillPath, params.name];
|
||||||
|
saveToHistory(newData, params.value, params.name, false, newPath);
|
||||||
|
|
||||||
// Update the chart with the new data structure
|
// Update the chart with the new data structure
|
||||||
option.series.data = newData;
|
option.series.data = newData;
|
||||||
@ -890,6 +900,9 @@ function renderChart(data) {
|
|||||||
|
|
||||||
// Update hover events with the new data structure, passing the drilled-down name
|
// Update hover events with the new data structure, passing the drilled-down name
|
||||||
setupHoverEvents({ total: params.value, data: newData }, params.name);
|
setupHoverEvents({ total: params.value, data: newData }, params.name);
|
||||||
|
|
||||||
|
// Update mini-chart previews to reflect drill-down
|
||||||
|
updateAllMonthPreviews();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1042,6 +1055,141 @@ function generateMonthPreviewGradient(data) {
|
|||||||
return `conic-gradient(${gradientStops.join(', ')})`;
|
return `conic-gradient(${gradientStops.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find category data at a given path in month's transaction data
|
||||||
|
function findCategoryDataAtPath(monthData, path) {
|
||||||
|
if (!path || path.length === 0) {
|
||||||
|
return null; // No drill-down, use full data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group transactions by the hierarchy at this path level
|
||||||
|
const categoryName = path[0];
|
||||||
|
|
||||||
|
// Filter transactions belonging to this category
|
||||||
|
const filteredTransactions = monthData.filter(item => item.category === categoryName);
|
||||||
|
|
||||||
|
if (filteredTransactions.length === 0) {
|
||||||
|
return { empty: true }; // Category doesn't exist in this month
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.length === 1) {
|
||||||
|
// Return subcategory breakdown
|
||||||
|
return { transactions: filteredTransactions, level: 'subcategory' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path length >= 2, filter by subcategory
|
||||||
|
const subcategoryName = path[1];
|
||||||
|
const subFiltered = filteredTransactions.filter(item => item.subcategory === subcategoryName);
|
||||||
|
|
||||||
|
if (subFiltered.length === 0) {
|
||||||
|
return { empty: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.length === 2) {
|
||||||
|
// Return microcategory breakdown
|
||||||
|
return { transactions: subFiltered, level: 'microcategory' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path length >= 3, filter by microcategory
|
||||||
|
const microcategoryName = path[2];
|
||||||
|
const microFiltered = subFiltered.filter(item => item.microcategory === microcategoryName);
|
||||||
|
|
||||||
|
if (microFiltered.length === 0) {
|
||||||
|
return { empty: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return transaction-level breakdown
|
||||||
|
return { transactions: microFiltered, level: 'transaction' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the base color for a drill-down path
|
||||||
|
function getPathBaseColor(path) {
|
||||||
|
if (!path || path.length === 0) {
|
||||||
|
return categoryColors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryName = path[0];
|
||||||
|
const categoryIndex = categoryOrder.indexOf(categoryName);
|
||||||
|
|
||||||
|
if (categoryIndex !== -1) {
|
||||||
|
return categoryColors[categoryIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryColors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate gradient for a month at the current drill-down path
|
||||||
|
function generateDrilledDownGradient(monthData, path) {
|
||||||
|
const result = findCategoryDataAtPath(monthData, path);
|
||||||
|
|
||||||
|
// No drill-down - use original function
|
||||||
|
if (!result) {
|
||||||
|
return generateMonthPreviewGradient(monthData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state - category doesn't exist
|
||||||
|
if (result.empty) {
|
||||||
|
return 'conic-gradient(#e0e0e0 0deg 360deg)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { transactions, level } = result;
|
||||||
|
|
||||||
|
// Group by the appropriate field based on level
|
||||||
|
const groupField = level === 'subcategory' ? 'subcategory'
|
||||||
|
: level === 'microcategory' ? 'microcategory'
|
||||||
|
: 'simple_name';
|
||||||
|
|
||||||
|
const totals = {};
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
transactions.forEach(item => {
|
||||||
|
const key = item[groupField] || '(без категории)';
|
||||||
|
const amount = Math.abs(parseFloat(item.amount_rub));
|
||||||
|
if (!isNaN(amount)) {
|
||||||
|
totals[key] = (totals[key] || 0) + amount;
|
||||||
|
total += amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return 'conic-gradient(#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);
|
||||||
|
|
||||||
|
const gradientStops = [];
|
||||||
|
let currentAngle = 0;
|
||||||
|
|
||||||
|
sortedKeys.forEach((key, index) => {
|
||||||
|
const percentage = totals[key] / total;
|
||||||
|
const angle = percentage * 360;
|
||||||
|
const color = colors[index] || categoryColors[index % categoryColors.length];
|
||||||
|
|
||||||
|
gradientStops.push(`${color} ${currentAngle}deg ${currentAngle + angle}deg`);
|
||||||
|
currentAngle += angle;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `conic-gradient(${gradientStops.join(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all month preview gradients based on current drill path
|
||||||
|
function updateAllMonthPreviews() {
|
||||||
|
const buttons = document.querySelectorAll('.month-btn');
|
||||||
|
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
const month = btn.dataset.month;
|
||||||
|
const preview = btn.querySelector('.month-preview');
|
||||||
|
|
||||||
|
if (preview && monthDataCache[month]) {
|
||||||
|
preview.style.background = generateDrilledDownGradient(monthDataCache[month], currentDrillPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Format month for display: "2025-01" -> "Январь'25"
|
// Format month for display: "2025-01" -> "Январь'25"
|
||||||
function formatMonthLabel(dateStr) {
|
function formatMonthLabel(dateStr) {
|
||||||
const [year, month] = dateStr.split('-');
|
const [year, month] = dateStr.split('-');
|
||||||
@ -1130,6 +1278,203 @@ 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'
|
||||||
|
];
|
||||||
|
|
||||||
|
const newData = [];
|
||||||
|
const sortedChildren = [...parentNode.children].sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
|
sortedChildren.forEach((child, i) => {
|
||||||
|
const color = colorPalette[i % colorPalette.length];
|
||||||
|
const newCategory = {
|
||||||
|
name: child.name,
|
||||||
|
value: child.value,
|
||||||
|
transactions: child.transactions,
|
||||||
|
itemStyle: { color: color },
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (child.children && child.children.length > 0) {
|
||||||
|
const sortedMicros = [...child.children].sort((a, b) => b.value - a.value);
|
||||||
|
const microColors = generateColorGradient(color, sortedMicros.length);
|
||||||
|
|
||||||
|
sortedMicros.forEach((micro, j) => {
|
||||||
|
const microCategory = {
|
||||||
|
name: micro.name,
|
||||||
|
value: micro.value,
|
||||||
|
transactions: micro.transactions,
|
||||||
|
itemStyle: { color: microColors[j] },
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (micro.transactions && micro.transactions.length > 0) {
|
||||||
|
const sortedTransactions = [...micro.transactions].sort((a, b) => b.value - a.value);
|
||||||
|
const transactionColors = generateColorGradient(microColors[j], sortedTransactions.length);
|
||||||
|
|
||||||
|
sortedTransactions.forEach((transaction, k) => {
|
||||||
|
microCategory.children.push({
|
||||||
|
name: transaction.name,
|
||||||
|
value: transaction.value,
|
||||||
|
itemStyle: { color: transactionColors[k] },
|
||||||
|
originalRow: transaction.originalRow
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newCategory.children.push(microCategory);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.transactions) {
|
||||||
|
const transactionsWithoutMicro = child.transactions.filter(t => t.displayMicrocategory === '');
|
||||||
|
if (transactionsWithoutMicro.length > 0) {
|
||||||
|
const transactionGroups = {};
|
||||||
|
transactionsWithoutMicro.forEach(t => {
|
||||||
|
if (!transactionGroups[t.name]) {
|
||||||
|
transactionGroups[t.name] = { name: t.name, value: 0, transactions: [] };
|
||||||
|
}
|
||||||
|
transactionGroups[t.name].value += t.value;
|
||||||
|
transactionGroups[t.name].transactions.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = Object.values(transactionGroups).sort((a, b) => b.value - a.value);
|
||||||
|
const transactionColors = generateColorGradient(color, groups.length);
|
||||||
|
|
||||||
|
groups.forEach((group, j) => {
|
||||||
|
const transactionCategory = {
|
||||||
|
name: group.name,
|
||||||
|
value: group.value,
|
||||||
|
transactions: group.transactions,
|
||||||
|
itemStyle: { color: transactionColors[j] },
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (group.transactions.length > 0) {
|
||||||
|
const sortedTransactions = [...group.transactions].sort((a, b) => b.value - a.value);
|
||||||
|
const individualColors = generateColorGradient(transactionColors[j], sortedTransactions.length);
|
||||||
|
|
||||||
|
sortedTransactions.forEach((transaction, k) => {
|
||||||
|
transactionCategory.children.push({
|
||||||
|
name: transaction.name,
|
||||||
|
value: transaction.value,
|
||||||
|
itemStyle: { color: individualColors[k] },
|
||||||
|
originalRow: transaction.originalRow
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newCategory.children.push(transactionCategory);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newData.push(newCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform transaction-only data for drill-down
|
||||||
|
function transformTransactionData(node) {
|
||||||
|
const colorPalette = [
|
||||||
|
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
|
||||||
|
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72'
|
||||||
|
];
|
||||||
|
|
||||||
|
const transactionGroups = {};
|
||||||
|
node.transactions.forEach(t => {
|
||||||
|
if (!transactionGroups[t.name]) {
|
||||||
|
transactionGroups[t.name] = { name: t.name, value: 0, transactions: [] };
|
||||||
|
}
|
||||||
|
transactionGroups[t.name].value += t.value;
|
||||||
|
transactionGroups[t.name].transactions.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newData = [];
|
||||||
|
const groups = Object.values(transactionGroups).sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
|
groups.forEach((group, i) => {
|
||||||
|
const color = colorPalette[i % colorPalette.length];
|
||||||
|
const transactionCategory = {
|
||||||
|
name: group.name,
|
||||||
|
value: group.value,
|
||||||
|
transactions: group.transactions,
|
||||||
|
itemStyle: { color: color },
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (group.transactions.length > 0) {
|
||||||
|
const sortedTransactions = [...group.transactions].sort((a, b) => b.value - a.value);
|
||||||
|
const transactionColors = generateColorGradient(color, sortedTransactions.length);
|
||||||
|
|
||||||
|
sortedTransactions.forEach((transaction, j) => {
|
||||||
|
transactionCategory.children.push({
|
||||||
|
name: transaction.name,
|
||||||
|
value: transaction.value,
|
||||||
|
itemStyle: { color: transactionColors[j] },
|
||||||
|
originalRow: transaction.originalRow
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newData.push(transactionCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to a path in the given sunburst data, returns drilled data or null if path not found
|
||||||
|
function navigateToPath(sunburstData, path) {
|
||||||
|
if (!path || path.length === 0) {
|
||||||
|
return null; // Stay at root
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find each level of the path
|
||||||
|
let currentData = sunburstData.data;
|
||||||
|
let currentTotal = sunburstData.total;
|
||||||
|
let currentName = null;
|
||||||
|
let validPath = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
const targetName = path[i];
|
||||||
|
const found = currentData.find(item => item.name === targetName);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
// Path level not found, return what we have so far
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
validPath.push(targetName);
|
||||||
|
currentName = targetName;
|
||||||
|
currentTotal = found.value;
|
||||||
|
|
||||||
|
// Transform this level's children to become top-level (same as click handler logic)
|
||||||
|
if (found.children && found.children.length > 0) {
|
||||||
|
currentData = transformDrillDownData(found);
|
||||||
|
} else if (found.transactions && found.transactions.length > 0) {
|
||||||
|
currentData = transformTransactionData(found);
|
||||||
|
} else {
|
||||||
|
// No more levels to drill into
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validPath.length === 0) {
|
||||||
|
return null; // Couldn't match any part of path
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: currentData,
|
||||||
|
total: currentTotal,
|
||||||
|
contextName: currentName,
|
||||||
|
path: validPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Select and load a specific month
|
// Select and load a specific month
|
||||||
async function selectMonth(index) {
|
async function selectMonth(index) {
|
||||||
currentMonthIndex = index;
|
currentMonthIndex = index;
|
||||||
@ -1143,7 +1488,7 @@ async function selectMonth(index) {
|
|||||||
updateMonthNavigator();
|
updateMonthNavigator();
|
||||||
|
|
||||||
// Load and render data
|
// Load and render data
|
||||||
const data = await parseCSV(`altcats-${month}.csv`);
|
const data = monthDataCache[month] || await parseCSV(`altcats-${month}.csv`);
|
||||||
|
|
||||||
// Check if chart already has data (for animation)
|
// Check if chart already has data (for animation)
|
||||||
if (option && option.series && option.series.data) {
|
if (option && option.series && option.series.data) {
|
||||||
@ -1152,32 +1497,49 @@ async function selectMonth(index) {
|
|||||||
// Update the module-level original data for center-click reset
|
// Update the module-level original data for center-click reset
|
||||||
originalSunburstData = JSON.parse(JSON.stringify(sunburstData));
|
originalSunburstData = JSON.parse(JSON.stringify(sunburstData));
|
||||||
|
|
||||||
// Reset and initialize history for the new month
|
// Save the current drill path before modifying state
|
||||||
|
const savedPath = [...currentDrillPath];
|
||||||
|
|
||||||
|
// Reset history for the new month
|
||||||
resetHistory();
|
resetHistory();
|
||||||
saveToHistory(sunburstData.data, sunburstData.total, null, true);
|
|
||||||
|
|
||||||
// Update only the series data and preserve layout
|
// Try to navigate to the same path in the new month
|
||||||
const oldData = option.series.data;
|
const navigatedState = navigateToPath(sunburstData, savedPath);
|
||||||
const newData = sunburstData.data;
|
|
||||||
|
|
||||||
// Map old values to new data to preserve positions
|
let targetData, targetTotal, targetName, targetPath;
|
||||||
newData.forEach((newItem, idx) => {
|
|
||||||
if (oldData[idx]) {
|
if (navigatedState) {
|
||||||
newItem.layoutId = oldData[idx].name;
|
// Successfully navigated to (part of) the path
|
||||||
|
targetData = navigatedState.data;
|
||||||
|
targetTotal = navigatedState.total;
|
||||||
|
targetName = navigatedState.contextName;
|
||||||
|
targetPath = navigatedState.path;
|
||||||
|
} else {
|
||||||
|
// Stay at root level
|
||||||
|
targetData = sunburstData.data;
|
||||||
|
targetTotal = sunburstData.total;
|
||||||
|
targetName = null;
|
||||||
|
targetPath = [];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Save initial state with the path
|
||||||
|
saveToHistory(targetData, targetTotal, targetName, true, targetPath);
|
||||||
|
|
||||||
// Update the data
|
// Update the data
|
||||||
option.series.data = newData;
|
option.series.data = targetData;
|
||||||
|
|
||||||
// Update the total amount in the center text
|
// Update the total amount in the center text
|
||||||
const russianMonth = getRussianMonthName(month);
|
const russianMonth = getRussianMonthName(month);
|
||||||
option.graphic.elements[0].style.text = russianMonth + '\n' + sunburstData.total.toFixed(0).toLocaleString() + ' ₽';
|
if (targetName) {
|
||||||
|
option.graphic.elements[0].style.text = `${russianMonth}\n${targetName}\n${targetTotal.toFixed(0).toLocaleString()} ₽`;
|
||||||
|
} else {
|
||||||
|
option.graphic.elements[0].style.text = russianMonth + '\n' + targetTotal.toFixed(0).toLocaleString() + ' ₽';
|
||||||
|
}
|
||||||
|
|
||||||
myChart.setOption({
|
myChart.setOption({
|
||||||
series: [{
|
series: [{
|
||||||
type: 'sunburst',
|
type: 'sunburst',
|
||||||
data: newData,
|
data: targetData,
|
||||||
layoutAnimation: true,
|
layoutAnimation: true,
|
||||||
animationDuration: 500,
|
animationDuration: 500,
|
||||||
animationEasing: 'cubicInOut'
|
animationEasing: 'cubicInOut'
|
||||||
@ -1189,7 +1551,10 @@ async function selectMonth(index) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update hover events
|
// Update hover events
|
||||||
setupHoverEvents(sunburstData);
|
setupHoverEvents({ total: targetTotal, data: targetData }, targetName);
|
||||||
|
|
||||||
|
// Update mini-chart previews
|
||||||
|
updateAllMonthPreviews();
|
||||||
} else {
|
} else {
|
||||||
// Initial render
|
// Initial render
|
||||||
renderChart(data);
|
renderChart(data);
|
||||||
@ -1563,6 +1928,15 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
nameSpan.appendChild(document.createTextNode(item.name));
|
nameSpan.appendChild(document.createTextNode(item.name));
|
||||||
itemDiv.appendChild(nameSpan);
|
itemDiv.appendChild(nameSpan);
|
||||||
|
|
||||||
|
// Add percentage after name if we have a parent value
|
||||||
|
if (parentValue) {
|
||||||
|
const percentSpan = document.createElement('span');
|
||||||
|
percentSpan.className = 'top-item-percent';
|
||||||
|
const percentage = ((item.value / parentValue) * 100).toFixed(1);
|
||||||
|
percentSpan.textContent = percentage + '%';
|
||||||
|
itemDiv.appendChild(percentSpan);
|
||||||
|
}
|
||||||
|
|
||||||
// Add eye button for viewing transaction details
|
// Add eye button for viewing transaction details
|
||||||
const eyeBtn = document.createElement('button');
|
const eyeBtn = document.createElement('button');
|
||||||
eyeBtn.className = 'eye-btn';
|
eyeBtn.className = 'eye-btn';
|
||||||
@ -1577,13 +1951,6 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
const amountSpan = document.createElement('span');
|
const amountSpan = document.createElement('span');
|
||||||
amountSpan.className = 'top-item-amount';
|
amountSpan.className = 'top-item-amount';
|
||||||
amountSpan.textContent = item.value.toLocaleString() + ' ₽';
|
amountSpan.textContent = item.value.toLocaleString() + ' ₽';
|
||||||
|
|
||||||
// Add percentage if we have a parent value
|
|
||||||
if (parentValue) {
|
|
||||||
const percentage = ((item.value / parentValue) * 100).toFixed(1);
|
|
||||||
amountSpan.textContent += ` (${percentage}%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemDiv.appendChild(amountSpan);
|
itemDiv.appendChild(amountSpan);
|
||||||
topItemsElement.appendChild(itemDiv);
|
topItemsElement.appendChild(itemDiv);
|
||||||
});
|
});
|
||||||
@ -1663,8 +2030,8 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show top categories as default items
|
// Show top categories as default items (sorted by value descending)
|
||||||
displayDetailsItems(sunburstData.data, sunburstData.total);
|
displayDetailsItems([...sunburstData.data].sort((a, b) => b.value - a.value), sunburstData.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show default view initially
|
// Show default view initially
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user