1/** 2 * Tax Burden Chart - Bar chart comparing optimized vs naive tax per year 3 */ 4 5let taxBurdenChart = null; 6 7// Two series, categorical slots 1-2 in sequence; solid fills, no strokes 8// (bars separate by the category gap, not by borders). 9function applyTaxBurdenTheme() { 10 if (!taxBurdenChart) return; 11 const P = QCChartTheme.palette(); 12 taxBurdenChart.data.datasets.forEach((ds, i) => { 13 ds.backgroundColor = P.data[i]; 14 }); 15} 16 17function initTaxBurdenChart() { 18 const ctx = document.getElementById('taxBurdenChart'); 19 if (!ctx) return; 20 21 taxBurdenChart = new Chart(ctx, { 22 type: 'bar', 23 data: { 24 labels: [], 25 datasets: [ 26 { 27 label: 'Recommended order', 28 data: [], 29 borderWidth: 0, 30 borderRadius: 3, 31 maxBarThickness: 24 32 }, 33 { 34 label: 'Tax + IRMAA', 35 data: [], 36 borderWidth: 0, 37 borderRadius: 3, 38 maxBarThickness: 24 39 } 40 ] 41 }, 42 options: { 43 responsive: true, 44 maintainAspectRatio: false, 45 interaction: { 46 intersect: false, 47 mode: 'index' 48 }, 49 plugins: { 50 legend: { 51 position: 'top', 52 align: 'start', 53 labels: { boxWidth: 8, boxHeight: 8, padding: 8, font: { size: 10 } } 54 }, 55 tooltip: { 56 callbacks: { 57 label: function(ctx) { 58 return ctx.dataset.label + ': ' + formatCurrency(ctx.parsed.y); 59 } 60 } 61 } 62 }, 63 scales: { 64 x: { 65 ticks: { 66 font: { size: 10 }, 67 maxRotation: 0, 68 autoSkip: true, 69 maxTicksLimit: 8 70 } 71 }, 72 y: { 73 // Keeps the axis on clean $ steps when a scenario pays 74 // no tax (empty series would otherwise tick $0..$1). 75 suggestedMax: 1000, 76 ticks: { 77 font: { size: 10 }, 78 callback: function(val) { return formatCurrency(val); } 79 } 80 } 81 } 82 } 83 }); 84 85 applyTaxBurdenTheme(); 86} 87 88QCChartTheme.register(() => { 89 if (!taxBurdenChart) return; 90 applyTaxBurdenTheme(); 91 QCChartTheme.refresh(taxBurdenChart); 92}); 93 94function updateTaxBurdenChart(yearlyTaxStats) { 95 if (!taxBurdenChart || !yearlyTaxStats || yearlyTaxStats.length === 0) return; 96 97 // Only show retirement years with tax data 98 const retireAge = AppState.inputs.retirementAge; 99 // Every retirement year, including the ones where the tax-aware order paid 100 // nothing: a chart that starts at the first taxed year (often 75, when RMDs 101 // begin) reads as missing data, when the flat zeros ARE the point. 102 const filtered = yearlyTaxStats.filter(yt => yt.age >= retireAge); 103 104 const labels = filtered.map(yt => 'Age ' + yt.age); 105 const taxOnly = filtered.map(yt => yt.taxPaid || 0); 106 const taxPlusIrmaa = filtered.map(yt => (yt.taxPaid || 0) + (yt.irmaaSurcharge || 0)); 107 108 taxBurdenChart.data.labels = labels; 109 taxBurdenChart.data.datasets[0].data = taxOnly; 110 taxBurdenChart.data.datasets[1].data = taxPlusIrmaa; 111 taxBurdenChart.update(); 112} 113 114// TEST SHIM (inert): classic scripts already expose these top-level 115// declarations as script globals in the browser, so these assignments 116// change nothing there. They exist so the vitest suite (which imports 117// this file as an ES module, where top-level declarations do NOT become 118// globals) can reach the functions via window. 119if (typeof window !== 'undefined') { 120 window.initTaxBurdenChart = initTaxBurdenChart; 121 window.updateTaxBurdenChart = updateTaxBurdenChart; 122}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.