From 58082e02f45c2439dba17cdec4daa2954f6ad280 Mon Sep 17 00:00:00 2001 From: Anton Volnuhin Date: Fri, 30 Jan 2026 19:48:58 +0300 Subject: [PATCH] Add browser history integration for drill-down navigation - Push drill-down states to browser history via history.pushState() - Listen for popstate event to handle browser back/forward - Mouse back/forward buttons and browser buttons now navigate drill-down - Click center still works (calls history.back()) - Reset details panel when mouse leaves chart sector Co-Authored-By: Claude Opus 4.5 --- app.js | 119 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index db22e57..c5e3013 100644 --- a/app.js +++ b/app.js @@ -4,6 +4,82 @@ const myChart = echarts.init(chartDom); let option; let originalSunburstData = null; // Stores the original data for the current month (for reset on center click) +// Drill-down history for back/forward navigation +let drillDownHistory = []; +let historyIndex = -1; + +// Save current chart state to history +function saveToHistory(data, total, contextName, isInitial = false) { + // Remove any forward history when drilling down from middle of history + if (historyIndex < drillDownHistory.length - 1) { + drillDownHistory = drillDownHistory.slice(0, historyIndex + 1); + } + drillDownHistory.push({ data, total, contextName }); + historyIndex = drillDownHistory.length - 1; + + // Push to browser history (use replaceState for initial state) + const state = { drillDown: historyIndex }; + if (isInitial) { + history.replaceState(state, ''); + } else { + history.pushState(state, ''); + } +} + +// Reset history (called when changing months) +function resetHistory() { + drillDownHistory = []; + historyIndex = -1; +} + +// Navigate to a specific history state +function navigateToHistoryState(state) { + const russianMonth = getRussianMonthName(document.getElementById('month-select').value); + option.series.data = state.data; + + if (state.contextName) { + option.graphic.elements[0].style.text = `${russianMonth}\n${state.contextName}\n${state.total.toFixed(0).toLocaleString()} ₽`; + } else { + option.graphic.elements[0].style.text = russianMonth + '\n' + state.total.toFixed(0).toLocaleString() + ' ₽'; + } + + myChart.setOption(option, { replaceMerge: ['series'] }); + setupHoverEvents({ total: state.total, data: state.data }, state.contextName); +} + +// Go back in drill-down history +function navigateBack() { + if (historyIndex > 0) { + historyIndex--; + navigateToHistoryState(drillDownHistory[historyIndex]); + } +} + +// Go forward in drill-down history +function navigateForward() { + if (historyIndex < drillDownHistory.length - 1) { + historyIndex++; + navigateToHistoryState(drillDownHistory[historyIndex]); + } +} + +// Listen for browser back/forward via popstate +window.addEventListener('popstate', function(e) { + if (e.state && e.state.drillDown !== undefined) { + const stateIndex = e.state.drillDown; + if (stateIndex >= 0 && stateIndex < drillDownHistory.length) { + historyIndex = stateIndex; + navigateToHistoryState(drillDownHistory[historyIndex]); + } + } else { + // No state or initial state - go to root + if (drillDownHistory.length > 0) { + historyIndex = 0; + navigateToHistoryState(drillDownHistory[0]); + } + } +}); + // Function to parse CSV data async function parseCSV(file) { const response = await fetch(file); @@ -322,6 +398,10 @@ function renderChart(data) { // Store the original data for resetting (module-level variable) originalSunburstData = JSON.parse(JSON.stringify(sunburstData)); + + // Reset and initialize history with the root state + resetHistory(); + saveToHistory(sunburstData.data, sunburstData.total, null, true); // Get the currently selected month const selectedMonth = document.getElementById('month-select').value; @@ -775,15 +855,18 @@ function renderChart(data) { }); } + // Save new state to history + saveToHistory(newData, params.value, params.name); + // Update the chart with the new data structure option.series.data = newData; - + // Update the center text to show the drilled-down category const russianMonth = getRussianMonthName(document.getElementById('month-select').value); option.graphic.elements[0].style.text = `${russianMonth}\n${params.name}\n${params.value.toFixed(0).toLocaleString()} ₽`; - + myChart.setOption(option, { replaceMerge: ['series'] }); - + // Update hover events with the new data structure, passing the drilled-down name setupHoverEvents({ total: params.value, data: newData }, params.name); } @@ -791,29 +874,23 @@ function renderChart(data) { myChart.setOption(option); - // Add click handler for the center to reset view + // Add click handler for the center to go back in history const zr = myChart.getZr(); zr.on('click', function(params) { const x = params.offsetX; const y = params.offsetY; - + // Calculate center and inner radius const chartWidth = myChart.getWidth(); const chartHeight = myChart.getHeight(); const centerX = chartWidth * (parseFloat(option.series.center[0]) / 100); const centerY = chartHeight * (parseFloat(option.series.center[1]) / 100); const innerRadius = Math.min(chartWidth, chartHeight) * 0.2; // 20% of chart size - + // Check if click is within the center circle const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); - if (distance < innerRadius) { - // Reset to original view - use module-level originalSunburstData - const currentMonth = document.getElementById('month-select').value; - const currentRussianMonth = getRussianMonthName(currentMonth); - option.series.data = originalSunburstData.data; - option.graphic.elements[0].style.text = currentRussianMonth + '\n' + originalSunburstData.total.toFixed(0).toLocaleString() + ' ₽'; - myChart.setOption(option, { replaceMerge: ['series'] }); - setupHoverEvents(originalSunburstData); + if (distance < innerRadius && historyIndex > 0) { + history.back(); // Use browser history - triggers popstate } }); @@ -1054,6 +1131,10 @@ async function selectMonth(index) { // Update the module-level original data for center-click reset originalSunburstData = JSON.parse(JSON.stringify(sunburstData)); + // Reset and initialize history for the new month + resetHistory(); + saveToHistory(sunburstData.data, sunburstData.total, null, true); + // Update only the series data and preserve layout const oldData = option.series.data; const newData = sunburstData.data; @@ -1723,10 +1804,10 @@ function setupHoverEvents(sunburstData, contextName = null) { myChart.on('mouseout', function(params) { if (params.data) { isOverChartSector = false; + isInsideSection = false; // Hide the floating eye button (unless hovering over it) hideChartEyeButton(); - // Reset details with a small delay to allow eye button mouseenter to fire first - // But only if we're not still inside another section + // Reset details with a small delay to allow mouseover of next sector to fire first setTimeout(() => { if (!isOverEyeButton && !isInsideSection) { showDefaultView(); @@ -1735,6 +1816,12 @@ function setupHoverEvents(sunburstData, contextName = null) { } }); + // Also reset when mouse leaves the chart container entirely + chartDom.addEventListener('mouseleave', function() { + isInsideSection = false; + showDefaultView(); + }); + // Add back the downplay event handler - this is triggered when sections lose emphasis myChart.on('downplay', function(params) { // Reset to default view when a section is no longer emphasized (unless hovering eye button or still in section)