@ -37,7 +37,6 @@ const myChart = echarts.init(chartDom);
let option ;
let originalSunburstData = null ; // Stores the original data for the current month (for reset on center click)
let showDefaultView = null ; // Reference to reset details panel to default view
let currentView = 'month' ; // 'month' | 'timeline'
// Drill-down history for back/forward navigation
let drillDownHistory = [ ] ;
@ -138,36 +137,8 @@ window.addEventListener('popstate', function(e) {
return ;
}
// No modal open - handle navigation
if ( e . state && e . state . timelineDrill !== undefined ) {
// Timeline drill-down navigation (possibly with legend selection)
if ( currentView !== 'timeline' ) {
currentView = 'timeline' ;
document . querySelectorAll ( '.view-switcher-btn' ) . forEach ( btn => {
btn . classList . toggle ( 'active' , btn . dataset . view === 'timeline' ) ;
} ) ;
document . querySelector ( '.container' ) . classList . add ( 'timeline-mode' ) ;
myChart . resize ( ) ;
}
renderTimelineChart ( e . state . timelineDrill , 'none' , e . state . legendSelected || null ) ;
return ;
}
if ( e . state && e . state . monthView ) {
// Month view navigation (from view switch)
if ( currentView !== 'month' ) {
switchView ( 'month' ) ;
}
return ;
}
// No modal open - handle drill-down navigation
if ( e . state && e . state . drillDown !== undefined ) {
// Donut drill-down navigation
if ( currentView !== 'month' ) {
// Switch from timeline to month view, then re-render donut from scratch
switchView ( 'month' ) ;
return ;
}
const stateIndex = e . state . drillDown ;
if ( stateIndex >= 0 && stateIndex < drillDownHistory . length ) {
historyIndex = stateIndex ;
@ -484,588 +455,6 @@ function transformToSunburst(data) {
} ;
}
// Build ECharts stacked bar option for timeline view
// drillPath: array like [] (categories), ['Cat'] (subcategories), ['Cat','Sub'] (microcategories)
function buildTimelineOption ( drillPath ) {
drillPath = drillPath || [ ] ;
const depth = drillPath . length ; // 0=category, 1=subcategory, 2=microcategory
const months = availableMonths ;
const xLabels = months . map ( m => formatMonthLabel ( m ) ) ;
const seriesList = [ ] ;
const legendData = [ ] ;
const emphLabel = {
show : true , position : 'top' ,
formatter : ( p ) => p . value >= 1000 ? Math . round ( p . value / 1000 ) + 'к' : '' ,
fontSize : 14 , fontWeight : 'bold' ,
fontFamily : '-apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", system-ui, sans-serif' ,
color : '#555' , backgroundColor : 'rgba(255,255,255,0.9)' , borderRadius : 2 , padding : [ 2 , 4 ]
} ;
// Hierarchy fields in order: category → subcategory → microcategory
const hierarchyFields = [ 'category' , 'subcategory' , 'microcategory' ] ;
// Collect totals grouped by the next level in the hierarchy
const groupTotals = { } ;
months . forEach ( ( month , mi ) => {
const data = monthDataCache [ month ] ;
if ( ! data ) return ;
data . forEach ( item => {
const amount = Math . abs ( parseFloat ( item . amount _rub ) ) ;
if ( isNaN ( amount ) ) return ;
// Filter: item must match all path segments
for ( let i = 0 ; i < depth ; i ++ ) {
const field = hierarchyFields [ i ] ;
const itemVal = item [ field ] || '' ;
if ( itemVal !== drillPath [ i ] ) return ;
}
// Group by the next field in the hierarchy
const groupField = hierarchyFields [ depth ] ;
const groupName = item [ groupField ] || 'Другое' ;
if ( ! groupTotals [ groupName ] ) {
groupTotals [ groupName ] = new Array ( months . length ) . fill ( 0 ) ;
}
groupTotals [ groupName ] [ mi ] += amount ;
} ) ;
} ) ;
// Build series with appropriate ordering and colors per depth level
if ( depth === 0 ) {
// Level 0: use categoryOrder and categoryColors
const usedCategories = new Set ( ) ;
categoryOrder . forEach ( ( catName , ci ) => {
if ( ! groupTotals [ catName ] ) return ;
usedCategories . add ( catName ) ;
legendData . push ( catName ) ;
seriesList . push ( {
name : catName ,
type : 'bar' ,
stack : 'total' ,
barMaxWidth : 50 ,
barCategoryGap : '35%' ,
data : groupTotals [ catName ] ,
itemStyle : { color : categoryColors [ ci ] } ,
label : { show : false } ,
emphasis : { focus : 'series' , label : emphLabel } ,
blur : { itemStyle : { opacity : 0.15 } }
} ) ;
} ) ;
Object . keys ( groupTotals ) . forEach ( catName => {
if ( usedCategories . has ( catName ) ) return ;
legendData . push ( catName ) ;
seriesList . push ( {
name : catName ,
type : 'bar' ,
stack : 'total' ,
barMaxWidth : 50 ,
barCategoryGap : '35%' ,
data : groupTotals [ catName ] ,
label : { show : false } ,
emphasis : { focus : 'series' , label : emphLabel } ,
blur : { itemStyle : { opacity : 0.15 } }
} ) ;
} ) ;
} else if ( depth === 1 ) {
// Level 1: use subcategoryOrder and getSubcategoryColor
const parentCategory = drillPath [ 0 ] ;
const order = subcategoryOrder [ parentCategory ] || [ ] ;
const usedSubs = new Set ( ) ;
let unknownIdx = 0 ;
order . forEach ( ( subName , si ) => {
if ( ! groupTotals [ subName ] ) return ;
usedSubs . add ( subName ) ;
legendData . push ( subName ) ;
seriesList . push ( {
name : subName ,
type : 'bar' ,
stack : 'total' ,
barMaxWidth : 50 ,
barCategoryGap : '35%' ,
data : groupTotals [ subName ] ,
itemStyle : { color : getSubcategoryColor ( parentCategory , subName , si ) } ,
label : { show : false } ,
emphasis : { focus : 'series' , label : emphLabel } ,
blur : { itemStyle : { opacity : 0.15 } }
} ) ;
} ) ;
Object . keys ( groupTotals ) . forEach ( subName => {
if ( usedSubs . has ( subName ) ) return ;
legendData . push ( subName ) ;
seriesList . push ( {
name : subName ,
type : 'bar' ,
stack : 'total' ,
barMaxWidth : 50 ,
barCategoryGap : '35%' ,
data : groupTotals [ subName ] ,
itemStyle : { color : getSubcategoryColor ( parentCategory , subName , 0 , unknownIdx ++ ) } ,
label : { show : false } ,
emphasis : { focus : 'series' , label : emphLabel } ,
blur : { itemStyle : { opacity : 0.15 } }
} ) ;
} ) ;
} else {
// Level 2 (microcategory): auto-colors from defaultColorPalette
let colorIdx = 0 ;
// Sort by total value descending for consistent ordering
const sortedNames = Object . keys ( groupTotals ) . sort ( ( a , b ) => {
const sumA = groupTotals [ a ] . reduce ( ( s , v ) => s + v , 0 ) ;
const sumB = groupTotals [ b ] . reduce ( ( s , v ) => s + v , 0 ) ;
return sumB - sumA ;
} ) ;
sortedNames . forEach ( name => {
legendData . push ( name ) ;
seriesList . push ( {
name : name ,
type : 'bar' ,
stack : 'total' ,
barMaxWidth : 50 ,
barCategoryGap : '35%' ,
data : groupTotals [ name ] ,
itemStyle : { color : defaultColorPalette [ colorIdx % defaultColorPalette . length ] } ,
label : { show : false } ,
emphasis : { focus : 'series' , label : emphLabel } ,
blur : { itemStyle : { opacity : 0.15 } }
} ) ;
colorIdx ++ ;
} ) ;
}
// Build per-series data map for dynamic total recalculation
const seriesDataMap = { } ;
seriesList . forEach ( s => { seriesDataMap [ s . name ] = s . data ; } ) ;
// Invisible "total" series on top — shows sum labels that update with legend selection
const totalPerMonth = new Array ( months . length ) . fill ( 0 ) ;
seriesList . forEach ( s => { s . data . forEach ( ( v , i ) => { totalPerMonth [ i ] += v ; } ) ; } ) ;
seriesList . push ( {
name : '__total__' ,
type : 'bar' ,
stack : 'total' ,
barMaxWidth : 50 ,
data : totalPerMonth . map ( ( ) => 1 ) ,
itemStyle : { color : 'transparent' } ,
label : {
show : true ,
position : 'top' ,
formatter : ( params ) => Math . round ( totalPerMonth [ params . dataIndex ] / 1000 ) + 'к' ,
fontSize : 14 ,
fontWeight : 'bold' ,
fontFamily : '-apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", system-ui, sans-serif' ,
color : '#666'
} ,
emphasis : { disabled : true } ,
blur : { label : { show : false } , itemStyle : { opacity : 0 } }
} ) ;
const isDrilled = drillPath . length > 0 ;
const chartTitle = isDrilled ? {
text : '← ' + drillPath . join ( ' › ' ) ,
left : 'center' ,
top : 6 ,
textStyle : {
fontSize : 22 ,
fontWeight : 'bold' ,
fontFamily : '-apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", system-ui, sans-serif' ,
color : '#333'
} ,
triggerEvent : true
} : { show : false } ;
const legendTop = isDrilled ? 42 : 10 ;
const legendRight = 10 ;
const legendItemWidth = 12 ;
const legendItemHeight = 12 ;
const legendItemGap = 8 ;
const legendPadding = [ 8 , 12 ] ;
const legendFontSize = 13 ;
return {
backgroundColor : '#fff' ,
title : chartTitle ,
tooltip : { show : false } ,
legend : {
data : legendData ,
top : legendTop ,
right : legendRight ,
orient : 'vertical' ,
itemWidth : legendItemWidth ,
itemHeight : legendItemHeight ,
textStyle : { fontSize : legendFontSize } ,
itemGap : legendItemGap ,
backgroundColor : 'rgba(255,255,255,0.85)' ,
borderRadius : 6 ,
padding : legendPadding ,
selector : false
} ,
grid : {
left : 30 ,
right : 180 ,
top : isDrilled ? 55 : 40 ,
bottom : 30 ,
containLabel : true
} ,
xAxis : {
type : 'category' ,
data : xLabels ,
axisLabel : { fontSize : 13 , rotate : - 45 }
} ,
yAxis : {
type : 'value' ,
axisLabel : {
formatter : function ( val ) {
if ( val >= 1000 ) return Math . round ( val / 1000 ) + 'к\u202F₽' ;
return val ;
} ,
fontSize : 13
}
} ,
series : seriesList ,
_seriesDataMap : seriesDataMap ,
_monthCount : months . length ,
_legendLayout : {
top : legendTop ,
right : legendRight ,
itemWidth : legendItemWidth ,
itemHeight : legendItemHeight ,
itemGap : legendItemGap ,
padding : legendPadding ,
fontSize : legendFontSize
}
} ;
}
let timelineDrillPath = [ ] ;
// Render (or re-render) the timeline chart, optionally drilled into a category path
// historyAction: 'push' (default, user drill-down), 'replace' (initial/view switch), 'none' (popstate restore)
function renderTimelineChart ( drillPath , historyAction , legendSelected ) {
drillPath = drillPath || [ ] ;
historyAction = historyAction || 'push' ;
timelineDrillPath = drillPath ;
window . _timelineResetLegend = null ;
myChart . off ( 'click' ) ;
myChart . off ( 'mouseover' ) ;
myChart . off ( 'mouseout' ) ;
myChart . off ( 'globalout' ) ;
myChart . off ( 'legendselectchanged' ) ;
// Clean up previous zrender label-click handler
if ( window . _timelineZrLabelHandler ) {
myChart . getZr ( ) . off ( 'click' , window . _timelineZrLabelHandler ) ;
window . _timelineZrLabelHandler = null ;
}
const tlOption = buildTimelineOption ( drillPath ) ;
myChart . clear ( ) ;
myChart . setOption ( tlOption , true ) ;
// Restore legend selection if provided (from popstate)
if ( legendSelected ) {
window . _suppressLegendHistory = true ;
Object . keys ( legendSelected ) . forEach ( name => {
myChart . dispatchAction ( {
type : legendSelected [ name ] === false ? 'legendUnSelect' : 'legendSelect' ,
name : name
} ) ;
} ) ;
window . _suppressLegendHistory = false ;
// Update totals and reset button for the restored selection
updateTotalsAndResetBtn ( legendSelected ) ;
}
// Update browser history state
const histState = { timelineDrill : drillPath } ;
if ( legendSelected ) histState . legendSelected = legendSelected ;
if ( historyAction === 'replace' ) {
history . replaceState ( histState , '' ) ;
} else if ( historyAction === 'push' ) {
history . pushState ( histState , '' ) ;
}
// 'none': skip history manipulation (popstate restore)
saveMonthSelectionState ( ) ;
// Highlight entire series on hover so all bars show emphasis.label
// Also highlight the corresponding legend item
const legendNames = tlOption . legend . data ;
let hlSeries = null ;
function updateLegend ( activeName ) {
myChart . setOption ( {
legend : {
data : legendNames . map ( name => {
if ( ! activeName ) return { name , textStyle : { fontWeight : 'normal' , color : '#333' } , itemStyle : { opacity : 1 } } ;
if ( name === activeName ) return { name , textStyle : { fontWeight : 'bold' , color : '#333' } , itemStyle : { opacity : 1 } } ;
return { name , textStyle : { fontWeight : 'normal' , color : '#ccc' } , itemStyle : { opacity : 0.15 } } ;
} )
}
} ) ;
}
myChart . on ( 'mouseover' , function ( params ) {
if ( params . componentType !== 'series' || params . seriesName === '__total__' ) return ;
if ( hlSeries === params . seriesName ) return ;
if ( hlSeries ) myChart . dispatchAction ( { type : 'downplay' , seriesName : hlSeries } ) ;
hlSeries = params . seriesName ;
myChart . dispatchAction ( { type : 'highlight' , seriesName : params . seriesName } ) ;
updateLegend ( params . seriesName ) ;
} ) ;
myChart . on ( 'mouseout' , function ( params ) {
if ( params . componentType !== 'series' || ! hlSeries ) return ;
myChart . dispatchAction ( { type : 'downplay' , seriesName : hlSeries } ) ;
hlSeries = null ;
updateLegend ( null ) ;
} ) ;
myChart . on ( 'globalout' , function ( ) {
if ( hlSeries ) {
myChart . dispatchAction ( { type : 'downplay' , seriesName : hlSeries } ) ;
hlSeries = null ;
updateLegend ( null ) ;
}
} ) ;
// Track whether all series are selected
function updateTotalsAndResetBtn ( selected ) {
const dataMap = tlOption . _seriesDataMap ;
const n = tlOption . _monthCount ;
const visibleTotals = new Array ( n ) . fill ( 0 ) ;
const allNames = tlOption . legend . data ;
const legendLayout = tlOption . _legendLayout ;
let allSelected = true ;
Object . keys ( dataMap ) . forEach ( name => {
if ( selected [ name ] !== false ) {
dataMap [ name ] . forEach ( ( v , i ) => { visibleTotals [ i ] += v ; } ) ;
} else {
allSelected = false ;
}
} ) ;
const legendFont = ` ${ legendLayout . fontSize } px sans-serif ` ;
const resetFontSize = Math . max ( 11 , legendLayout . fontSize - 1 ) ;
const resetFont = ` ${ resetFontSize } px sans-serif ` ;
const resetXOffset = 3 ;
const maxLegendTextWidth = allNames . reduce ( ( maxWidth , name ) => {
return Math . max ( maxWidth , echarts . format . getTextRect ( name , legendFont ) . width ) ;
} , 0 ) ;
const iconTextGap = 5 ; // ECharts default icon/text gap for legend items
const legendBoxWidth =
legendLayout . padding [ 1 ] * 2 +
legendLayout . itemWidth +
iconTextGap +
maxLegendTextWidth ;
const resetTextWidth = echarts . format . getTextRect ( '× сбросить' , resetFont ) . width ;
const resetRight =
legendLayout . right +
legendBoxWidth -
legendLayout . padding [ 1 ] -
resetTextWidth -
resetXOffset ;
const legendView = ( myChart . _componentsViews || [ ] ) . find ( v => v && v . _ _model && v . _ _model . mainType === 'legend' ) ;
const resetLeft = legendView && legendView . group ? legendView . group . x : null ;
let resetTop ;
if ( legendView && legendView . group && legendView . _contentGroup ) {
const legendItems = legendView . _contentGroup . children ( ) ;
if ( legendItems . length > 0 ) {
const lastItem = legendItems [ legendItems . length - 1 ] ;
const prevItem = legendItems . length > 1 ? legendItems [ legendItems . length - 2 ] : null ;
const rowStep = prevItem ? ( lastItem . y - prevItem . y ) : ( legendLayout . itemHeight + legendLayout . itemGap ) ;
const lastRect = lastItem . getBoundingRect ( ) ;
resetTop =
legendView . group . y +
( legendView . _contentGroup . y || 0 ) +
lastItem . y +
lastRect . y +
rowStep ;
}
}
if ( resetTop === undefined ) {
const nextRowTop =
legendLayout . top +
legendLayout . padding [ 0 ] +
allNames . length * ( legendLayout . itemHeight + legendLayout . itemGap ) ;
resetTop = nextRowTop + 11 ;
}
const resetLegendSelection = function ( ) {
window . _suppressLegendHistory = true ;
allNames . forEach ( name => {
myChart . dispatchAction ( { type : 'legendSelect' , name : name } ) ;
} ) ;
window . _suppressLegendHistory = false ;
const resetSelected = { } ;
allNames . forEach ( name => { resetSelected [ name ] = true ; } ) ;
updateTotalsAndResetBtn ( resetSelected ) ;
history . pushState ( { timelineDrill : drillPath } , '' ) ;
} ;
window . _timelineResetLegend = allSelected ? null : resetLegendSelection ;
const resetGraphic = {
type : 'text' ,
id : 'resetBtn' ,
top : resetTop ,
z : 100 ,
zlevel : 1 ,
style : {
text : allSelected ? '' : '× сбросить' ,
fontSize : resetFontSize ,
fill : '#999' ,
fontFamily : '-apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", system-ui, sans-serif'
} ,
cursor : 'pointer' ,
onmouseover : function ( ) {
if ( this && this . setStyle ) this . setStyle ( { fill : '#000' } ) ;
} ,
onmouseout : function ( ) {
if ( this && this . setStyle ) this . setStyle ( { fill : '#999' } ) ;
} ,
onclick : resetLegendSelection
} ;
if ( resetLeft !== null ) {
resetGraphic . left = resetLeft + resetXOffset ;
} else {
resetGraphic . right = resetRight ;
}
myChart . setOption ( {
series : [ { name : '__total__' , label : {
formatter : ( p ) => Math . round ( visibleTotals [ p . dataIndex ] / 1000 ) + 'к'
} } ] ,
graphic : [ resetGraphic ]
} ) ;
}
// Update total labels when legend selection changes (cmd-click / opt-click / legend click)
myChart . on ( 'legendselectchanged' , function ( params ) {
updateTotalsAndResetBtn ( params . selected ) ;
// Push legend selection to browser history (skip during popstate restore)
if ( ! window . _suppressLegendHistory ) {
history . pushState ( { timelineDrill : drillPath , legendSelected : params . selected } , '' ) ;
}
} ) ;
// Click handler: drill deeper or go up via title
myChart . on ( 'click' , function ( params ) {
if ( params . componentType === 'title' ) {
// Go up one level
renderTimelineChart ( drillPath . slice ( 0 , - 1 ) ) ;
return ;
}
if ( params . componentType === 'series' && params . seriesName !== '__total__' ) {
const nativeEvent = params . event && params . event . event ;
// Cmd-click: select only this series (hide all others)
if ( nativeEvent && nativeEvent . metaKey ) {
const allNames = tlOption . legend . data ;
window . _suppressLegendHistory = true ;
allNames . forEach ( name => {
myChart . dispatchAction ( {
type : name === params . seriesName ? 'legendSelect' : 'legendUnSelect' ,
name : name
} ) ;
} ) ;
window . _suppressLegendHistory = false ;
// Build selected map and push once
const selected = { } ;
allNames . forEach ( name => { selected [ name ] = name === params . seriesName ; } ) ;
updateTotalsAndResetBtn ( selected ) ;
history . pushState ( { timelineDrill : drillPath , legendSelected : selected } , '' ) ;
return ;
}
// Opt-click: toggle this series off (single action — legendselectchanged handles push)
if ( nativeEvent && nativeEvent . altKey ) {
myChart . dispatchAction ( { type : 'legendToggleSelect' , name : params . seriesName } ) ;
return ;
}
// Plain click: drill deeper if not at max depth (2 = microcategory level)
if ( drillPath . length < 2 ) {
renderTimelineChart ( [ ... drillPath , params . seriesName ] ) ;
}
}
} ) ;
// Zrender-level click: detect clicks in the x-axis label area → switch to that month
window . _timelineZrLabelHandler = function ( e ) {
const y = e . offsetY ;
const yBottom = myChart . convertToPixel ( { yAxisIndex : 0 } , 0 ) ;
// Click must be below the bar area (in the label zone)
if ( y > yBottom + 5 ) {
const x = e . offsetX ;
// Find nearest bar index
let bestIdx = - 1 , bestDist = Infinity ;
for ( let i = 0 ; i < availableMonths . length ; i ++ ) {
const barX = myChart . convertToPixel ( { xAxisIndex : 0 } , i ) ;
const dist = Math . abs ( x - barX ) ;
if ( dist < bestDist ) { bestDist = dist ; bestIdx = i ; }
}
// Accept if within half the bar spacing
const spacing = availableMonths . length > 1
? Math . abs ( myChart . convertToPixel ( { xAxisIndex : 0 } , 1 ) - myChart . convertToPixel ( { xAxisIndex : 0 } , 0 ) )
: 100 ;
if ( bestIdx >= 0 && bestDist < spacing * 0.6 ) {
history . pushState ( { monthView : true } , '' ) ;
switchView ( 'month' ) ;
selectSingleMonth ( bestIdx ) ;
}
}
} ;
myChart . getZr ( ) . on ( 'click' , window . _timelineZrLabelHandler ) ;
}
// Switch between month (sunburst) and timeline (stacked bar) views
function switchView ( viewName ) {
if ( viewName === currentView ) return ;
currentView = viewName ;
// Update switcher button active state
document . querySelectorAll ( '.view-switcher-btn' ) . forEach ( btn => {
btn . classList . toggle ( 'active' , btn . dataset . view === viewName ) ;
} ) ;
const container = document . querySelector ( '.container' ) ;
if ( viewName === 'timeline' ) {
container . classList . add ( 'timeline-mode' ) ;
// Remove all chart event listeners
myChart . off ( 'click' ) ;
myChart . off ( 'mouseover' ) ;
myChart . off ( 'mouseout' ) ;
myChart . off ( 'globalout' ) ;
myChart . off ( 'legendselectchanged' ) ;
// Resize after layout change, then render stacked bar
myChart . resize ( ) ;
renderTimelineChart ( timelineDrillPath , 'replace' ) ;
// Ensure resize after CSS layout fully settles (needed for deferred restore)
requestAnimationFrame ( ( ) => myChart . resize ( ) ) ;
} else {
container . classList . remove ( 'timeline-mode' ) ;
window . _timelineResetLegend = null ;
// Remove all chart event listeners
myChart . off ( 'click' ) ;
myChart . off ( 'mouseover' ) ;
myChart . off ( 'mouseout' ) ;
myChart . off ( 'globalout' ) ;
myChart . off ( 'legendselectchanged' ) ;
// Clear chart completely so no bar chart remnants remain
myChart . clear ( ) ;
option = null ;
// Resize after layout change, then re-render sunburst from scratch
myChart . resize ( ) ;
renderSelectedMonths ( ) ;
}
saveMonthSelectionState ( ) ;
}
// Function to get Russian month name from YYYY-MM format
function getRussianMonthName ( dateStr ) {
const monthNum = parseInt ( dateStr . split ( '-' ) [ 1 ] ) ;
@ -1691,9 +1080,7 @@ function saveMonthSelectionState() {
localStorage . setItem ( MONTH _SELECTION _STORAGE _KEY , JSON . stringify ( {
currentMonth ,
selectedMonths ,
drillPath : [ ... currentDrillPath ] ,
view : currentView ,
timelineDrillPath : [ ... timelineDrillPath ]
drillPath : [ ... currentDrillPath ]
} ) ) ;
} catch ( error ) {
// Ignore storage failures (private mode, disabled storage, etc.)
@ -1733,16 +1120,6 @@ function restoreMonthSelectionState() {
currentDrillPath = [ ] ;
}
// Restore timeline drill path if saved
if ( Array . isArray ( saved . timelineDrillPath ) ) {
timelineDrillPath = [ ... saved . timelineDrillPath ] ;
}
// Defer timeline restore so chart is initialized first
if ( saved . view === 'timeline' ) {
requestAnimationFrame ( ( ) => switchView ( 'timeline' ) ) ;
}
return true ;
} catch ( error ) {
return false ;
@ -2198,17 +1575,6 @@ async function loadAvailableMonths() {
}
} ) ;
}
// Set up view switcher
document . querySelectorAll ( '.view-switcher-btn' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => {
const target = btn . dataset . view ;
if ( target !== currentView ) {
history . pushState ( target === 'timeline' ? { timelineDrill : timelineDrillPath } : { monthView : true } , '' ) ;
}
switchView ( target ) ;
} ) ;
} ) ;
}
// Transform children data for drill-down (extracted from click handler)
@ -2710,10 +2076,10 @@ initVisualization();
// Handle window resize
window . addEventListener ( 'resize' , function ( ) {
if ( currentView === 'month' ) {
adjustChartSize ( ) ;
}
adjustChartSize ( ) ;
myChart . resize ( ) ;
// Recalculate chart center and radius for hover detection will be done automatically by setupHoverEvents
} ) ;
// Function to adjust chart size based on screen width
@ -3621,9 +2987,6 @@ function handleGlobalEscape(e) {
closeRowDetailModal ( ) ;
} else if ( transactionModal . style . display !== 'none' ) {
closeTransactionModal ( ) ;
} else if ( currentView === 'timeline' && typeof window . _timelineResetLegend === 'function' ) {
window . _timelineResetLegend ( ) ;
e . preventDefault ( ) ;
}
}
}