fix: preserve month and drill-down state across reload

This commit is contained in:
Anton Volnuhin 2026-02-06 12:24:50 +03:00
parent 95fea028d8
commit 44a97c2ca8

125
app.js
View File

@ -62,6 +62,8 @@ function saveToHistory(data, total, contextName, isInitial = false, path = []) {
} else { } else {
history.pushState(state, ''); history.pushState(state, '');
} }
saveMonthSelectionState();
} }
// Reset history (called when changing months) // Reset history (called when changing months)
@ -86,6 +88,7 @@ function navigateToHistoryState(state) {
// Restore drill path and update mini-charts // Restore drill path and update mini-charts
currentDrillPath = state.path ? [...state.path] : []; currentDrillPath = state.path ? [...state.path] : [];
updateAllMonthPreviews(); updateAllMonthPreviews();
saveMonthSelectionState();
} }
// Go back in drill-down history // Go back in drill-down history
@ -465,6 +468,7 @@ function getRussianMonthName(dateStr) {
// Function to render the chart // Function to render the chart
function renderChart(data) { function renderChart(data) {
const sunburstData = transformToSunburst(data); const sunburstData = transformToSunburst(data);
const restoredPath = [...currentDrillPath];
// Store the original data for resetting (module-level variable) // Store the original data for resetting (module-level variable)
originalSunburstData = JSON.parse(JSON.stringify(sunburstData)); originalSunburstData = JSON.parse(JSON.stringify(sunburstData));
@ -961,8 +965,21 @@ function renderChart(data) {
myChart.setOption(option); myChart.setOption(option);
// Try to restore drill-down path on initial render (e.g., after page reload)
const restoredState = navigateToPath(sunburstData, restoredPath);
if (restoredState) {
option.series.data = restoredState.data;
myChart.setOption(option, { replaceMerge: ['series'] });
saveToHistory(restoredState.data, restoredState.total, restoredState.contextName, false, restoredState.path);
updateCenterLabel(russianMonth, restoredState.total, restoredState.contextName);
setupHoverEvents({ total: restoredState.total, data: restoredState.data }, restoredState.contextName);
updateAllMonthPreviews();
} else {
// Update HTML center label // Update HTML center label
updateCenterLabel(russianMonth, sunburstData.total); updateCenterLabel(russianMonth, sunburstData.total);
// Set up hover events for the details box
setupHoverEvents(sunburstData);
}
// Add click handler for the center to go back in history // Add click handler for the center to go back in history
const zr = myChart.getZr(); const zr = myChart.getZr();
@ -984,9 +1001,6 @@ function renderChart(data) {
} }
}); });
// Set up hover events for the details box
setupHoverEvents(sunburstData);
// Ensure chart is properly sized after rendering // Ensure chart is properly sized after rendering
adjustChartSize(); adjustChartSize();
myChart.resize(); myChart.resize();
@ -1054,6 +1068,63 @@ let availableMonths = [];
let currentMonthIndex = 0; let currentMonthIndex = 0;
let selectedMonthIndices = new Set(); // Track all selected months for multi-selection let selectedMonthIndices = new Set(); // Track all selected months for multi-selection
let monthDataCache = {}; // Cache for month data previews let monthDataCache = {}; // Cache for month data previews
const MONTH_SELECTION_STORAGE_KEY = 'visual-spending:month-selection';
function saveMonthSelectionState() {
try {
const selectedMonths = [...selectedMonthIndices]
.map(index => availableMonths[index])
.filter(Boolean);
const currentMonth = availableMonths[currentMonthIndex] || null;
localStorage.setItem(MONTH_SELECTION_STORAGE_KEY, JSON.stringify({
currentMonth,
selectedMonths,
drillPath: [...currentDrillPath]
}));
} catch (error) {
// Ignore storage failures (private mode, disabled storage, etc.)
}
}
function restoreMonthSelectionState() {
try {
const raw = localStorage.getItem(MONTH_SELECTION_STORAGE_KEY);
if (!raw) return false;
const saved = JSON.parse(raw);
if (!saved || !Array.isArray(saved.selectedMonths) || saved.selectedMonths.length === 0) {
return false;
}
const restoredIndices = saved.selectedMonths
.map(month => availableMonths.indexOf(month))
.filter(index => index >= 0);
if (restoredIndices.length === 0) {
return false;
}
selectedMonthIndices = new Set(restoredIndices);
const restoredCurrentIndex = availableMonths.indexOf(saved.currentMonth);
if (restoredCurrentIndex >= 0 && selectedMonthIndices.has(restoredCurrentIndex)) {
currentMonthIndex = restoredCurrentIndex;
} else {
currentMonthIndex = Math.max(...selectedMonthIndices);
}
if (Array.isArray(saved.drillPath)) {
currentDrillPath = [...saved.drillPath];
} else {
currentDrillPath = [];
}
return true;
} catch (error) {
return false;
}
}
// Predefined colors for categories (same as in transformToSunburst) // Predefined colors for categories (same as in transformToSunburst)
const categoryColors = [ const categoryColors = [
@ -1451,11 +1522,20 @@ async function loadAvailableMonths() {
monthList.appendChild(btn); monthList.appendChild(btn);
}); });
// Load the most recent month by default // Restore previous month selection when possible, otherwise default to latest month
if (restoreMonthSelectionState()) {
if (selectedMonthIndices.size > 1) {
await renderSelectedMonths();
} else {
await selectMonth(currentMonthIndex);
}
} else {
currentMonthIndex = availableMonths.length - 1; currentMonthIndex = availableMonths.length - 1;
selectedMonthIndices.clear(); selectedMonthIndices.clear();
selectedMonthIndices.add(currentMonthIndex); selectedMonthIndices.add(currentMonthIndex);
await selectMonth(currentMonthIndex); await selectMonth(currentMonthIndex);
saveMonthSelectionState();
}
// Set up arrow button handlers // Set up arrow button handlers
document.getElementById('prev-month').addEventListener('click', (event) => { document.getElementById('prev-month').addEventListener('click', (event) => {
@ -1708,6 +1788,7 @@ async function toggleMonthSelection(index) {
currentMonthIndex = index; // Update navigation index to the newly added month currentMonthIndex = index; // Update navigation index to the newly added month
} }
await renderSelectedMonths(); await renderSelectedMonths();
saveMonthSelectionState();
} }
// Select a single month (normal click behavior) // Select a single month (normal click behavior)
@ -1716,6 +1797,7 @@ async function selectSingleMonth(index) {
selectedMonthIndices.add(index); selectedMonthIndices.add(index);
currentMonthIndex = index; currentMonthIndex = index;
await selectMonth(index); await selectMonth(index);
saveMonthSelectionState();
} }
// Select interval from current month to target (Shift+click behavior) // Select interval from current month to target (Shift+click behavior)
@ -1729,6 +1811,7 @@ async function selectMonthInterval(targetIndex) {
} }
currentMonthIndex = targetIndex; currentMonthIndex = targetIndex;
await renderSelectedMonths(); await renderSelectedMonths();
saveMonthSelectionState();
} }
// Merge transaction data from multiple months // Merge transaction data from multiple months
@ -1781,6 +1864,7 @@ function generateSelectedMonthsLabel() {
async function renderSelectedMonths() { async function renderSelectedMonths() {
// Merge data from all selected months // Merge data from all selected months
const mergedData = mergeMonthsData(selectedMonthIndices); const mergedData = mergeMonthsData(selectedMonthIndices);
const savedPath = [...currentDrillPath];
// Update UI // Update UI
updateMonthNavigator(); updateMonthNavigator();
@ -1801,16 +1885,35 @@ async function renderSelectedMonths() {
// Reset history for the new selection // Reset history for the new selection
resetHistory(); resetHistory();
// Try to restore the same drill-down path in merged view
const navigatedState = navigateToPath(sunburstData, savedPath);
let targetData, targetTotal, targetName, targetPath;
if (navigatedState) {
saveToHistory(sunburstData.data, sunburstData.total, null, true, []); saveToHistory(sunburstData.data, sunburstData.total, null, true, []);
targetData = navigatedState.data;
targetTotal = navigatedState.total;
targetName = navigatedState.contextName;
targetPath = navigatedState.path;
saveToHistory(targetData, targetTotal, targetName, false, targetPath);
} else {
targetData = sunburstData.data;
targetTotal = sunburstData.total;
targetName = null;
targetPath = [];
saveToHistory(targetData, targetTotal, targetName, true, targetPath);
}
// Update the chart // Update the chart
if (option && option.series && option.series.data) { if (option && option.series && option.series.data) {
option.series.data = sunburstData.data; option.series.data = targetData;
myChart.setOption({ myChart.setOption({
series: [{ series: [{
type: 'sunburst', type: 'sunburst',
data: sunburstData.data, data: targetData,
layoutAnimation: true, layoutAnimation: true,
animationDuration: 500, animationDuration: 500,
animationEasing: 'cubicInOut' animationEasing: 'cubicInOut'
@ -1820,8 +1923,8 @@ async function renderSelectedMonths() {
silent: false silent: false
}); });
updateCenterLabel(monthLabel, sunburstData.total, null); updateCenterLabel(monthLabel, targetTotal, targetName);
setupHoverEvents(sunburstData, null); setupHoverEvents({ total: targetTotal, data: targetData }, targetName);
updateAllMonthPreviews(); updateAllMonthPreviews();
} else { } else {
// Initial render // Initial render
@ -1833,6 +1936,8 @@ async function renderSelectedMonths() {
if (activeBtn) { if (activeBtn) {
activeBtn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); activeBtn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
} }
saveMonthSelectionState();
} }
// Select and load a specific month // Select and load a specific month
@ -1840,7 +1945,7 @@ async function selectMonth(index) {
const month = availableMonths[index]; const month = availableMonths[index];
// If clicking on the already-selected month while drilled down, reset to root // If clicking on the already-selected month while drilled down, reset to root
if (index === currentMonthIndex && currentDrillPath.length > 0) { if (index === currentMonthIndex && currentDrillPath.length > 0 && drillDownHistory.length > 0) {
goToRoot(); goToRoot();
return; return;
} }
@ -1934,6 +2039,8 @@ async function selectMonth(index) {
if (activeBtn) { if (activeBtn) {
activeBtn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); activeBtn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
} }
saveMonthSelectionState();
} }
// Update month navigator UI state // Update month navigator UI state