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 <noreply@anthropic.com>
This commit is contained in:
parent
08db280f84
commit
58082e02f4
119
app.js
119
app.js
@ -4,6 +4,82 @@ const myChart = echarts.init(chartDom);
|
|||||||
let option;
|
let option;
|
||||||
let originalSunburstData = null; // Stores the original data for the current month (for reset on center click)
|
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
|
// Function to parse CSV data
|
||||||
async function parseCSV(file) {
|
async function parseCSV(file) {
|
||||||
const response = await fetch(file);
|
const response = await fetch(file);
|
||||||
@ -322,6 +398,10 @@ function renderChart(data) {
|
|||||||
|
|
||||||
// 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));
|
||||||
|
|
||||||
|
// Reset and initialize history with the root state
|
||||||
|
resetHistory();
|
||||||
|
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;
|
||||||
@ -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
|
// Update the chart with the new data structure
|
||||||
option.series.data = newData;
|
option.series.data = newData;
|
||||||
|
|
||||||
// Update the center text to show the drilled-down category
|
// Update the center text to show the drilled-down category
|
||||||
const russianMonth = getRussianMonthName(document.getElementById('month-select').value);
|
const russianMonth = getRussianMonthName(document.getElementById('month-select').value);
|
||||||
option.graphic.elements[0].style.text = `${russianMonth}\n${params.name}\n${params.value.toFixed(0).toLocaleString()} ₽`;
|
option.graphic.elements[0].style.text = `${russianMonth}\n${params.name}\n${params.value.toFixed(0).toLocaleString()} ₽`;
|
||||||
|
|
||||||
myChart.setOption(option, { replaceMerge: ['series'] });
|
myChart.setOption(option, { replaceMerge: ['series'] });
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
@ -791,29 +874,23 @@ function renderChart(data) {
|
|||||||
|
|
||||||
myChart.setOption(option);
|
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();
|
const zr = myChart.getZr();
|
||||||
zr.on('click', function(params) {
|
zr.on('click', function(params) {
|
||||||
const x = params.offsetX;
|
const x = params.offsetX;
|
||||||
const y = params.offsetY;
|
const y = params.offsetY;
|
||||||
|
|
||||||
// Calculate center and inner radius
|
// Calculate center and inner radius
|
||||||
const chartWidth = myChart.getWidth();
|
const chartWidth = myChart.getWidth();
|
||||||
const chartHeight = myChart.getHeight();
|
const chartHeight = myChart.getHeight();
|
||||||
const centerX = chartWidth * (parseFloat(option.series.center[0]) / 100);
|
const centerX = chartWidth * (parseFloat(option.series.center[0]) / 100);
|
||||||
const centerY = chartHeight * (parseFloat(option.series.center[1]) / 100);
|
const centerY = chartHeight * (parseFloat(option.series.center[1]) / 100);
|
||||||
const innerRadius = Math.min(chartWidth, chartHeight) * 0.2; // 20% of chart size
|
const innerRadius = Math.min(chartWidth, chartHeight) * 0.2; // 20% of chart size
|
||||||
|
|
||||||
// Check if click is within the center circle
|
// Check if click is within the center circle
|
||||||
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
||||||
if (distance < innerRadius) {
|
if (distance < innerRadius && historyIndex > 0) {
|
||||||
// Reset to original view - use module-level originalSunburstData
|
history.back(); // Use browser history - triggers popstate
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1054,6 +1131,10 @@ 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
|
||||||
|
resetHistory();
|
||||||
|
saveToHistory(sunburstData.data, sunburstData.total, null, true);
|
||||||
|
|
||||||
// Update only the series data and preserve layout
|
// Update only the series data and preserve layout
|
||||||
const oldData = option.series.data;
|
const oldData = option.series.data;
|
||||||
const newData = sunburstData.data;
|
const newData = sunburstData.data;
|
||||||
@ -1723,10 +1804,10 @@ function setupHoverEvents(sunburstData, contextName = null) {
|
|||||||
myChart.on('mouseout', function(params) {
|
myChart.on('mouseout', function(params) {
|
||||||
if (params.data) {
|
if (params.data) {
|
||||||
isOverChartSector = false;
|
isOverChartSector = false;
|
||||||
|
isInsideSection = false;
|
||||||
// Hide the floating eye button (unless hovering over it)
|
// Hide the floating eye button (unless hovering over it)
|
||||||
hideChartEyeButton();
|
hideChartEyeButton();
|
||||||
// Reset details with a small delay to allow eye button mouseenter to fire first
|
// Reset details with a small delay to allow mouseover of next sector to fire first
|
||||||
// But only if we're not still inside another section
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!isOverEyeButton && !isInsideSection) {
|
if (!isOverEyeButton && !isInsideSection) {
|
||||||
showDefaultView();
|
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
|
// Add back the downplay event handler - this is triggered when sections lose emphasis
|
||||||
myChart.on('downplay', function(params) {
|
myChart.on('downplay', function(params) {
|
||||||
// Reset to default view when a section is no longer emphasized (unless hovering eye button or still in section)
|
// Reset to default view when a section is no longer emphasized (unless hovering eye button or still in section)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user