Enhance cospend monthly expenses chart with improved UX
- Add monthly total labels above each bar showing cumulative expense amounts - Improve chart styling: white labels, larger fonts, clean flat tooltip design - Hide Y-axis ticks and grid lines for cleaner appearance - Capitalize category names in legend and tooltips - Show only hovered category in tooltip instead of all categories - Trim empty months from start of data for users with limited history - Create responsive layout: balance and chart side-by-side on wide screens - Increase max width to 1400px for dashboard while keeping recent activity at 800px - Filter out settlements from monthly expenses view 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		@@ -53,9 +53,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const ctx = canvas.getContext('2d');
 | 
					    const ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Process datasets with colors
 | 
					    // Process datasets with colors and capitalize labels
 | 
				
			||||||
    const processedDatasets = data.datasets.map((dataset, index) => ({
 | 
					    const processedDatasets = data.datasets.map((dataset, index) => ({
 | 
				
			||||||
      ...dataset,
 | 
					      ...dataset,
 | 
				
			||||||
 | 
					      label: dataset.label.charAt(0).toUpperCase() + dataset.label.slice(1),
 | 
				
			||||||
      backgroundColor: getCategoryColor(dataset.label, index),
 | 
					      backgroundColor: getCategoryColor(dataset.label, index),
 | 
				
			||||||
      borderColor: getCategoryColor(dataset.label, index),
 | 
					      borderColor: getCategoryColor(dataset.label, index),
 | 
				
			||||||
      borderWidth: 1
 | 
					      borderWidth: 1
 | 
				
			||||||
@@ -70,16 +71,26 @@
 | 
				
			|||||||
      options: {
 | 
					      options: {
 | 
				
			||||||
        responsive: true,
 | 
					        responsive: true,
 | 
				
			||||||
        maintainAspectRatio: false,
 | 
					        maintainAspectRatio: false,
 | 
				
			||||||
 | 
					        layout: {
 | 
				
			||||||
 | 
					          padding: {
 | 
				
			||||||
 | 
					            top: 40
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        scales: {
 | 
					        scales: {
 | 
				
			||||||
          x: {
 | 
					          x: {
 | 
				
			||||||
            stacked: true,
 | 
					            stacked: true,
 | 
				
			||||||
            grid: {
 | 
					            grid: {
 | 
				
			||||||
              display: false
 | 
					              display: false
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            border: {
 | 
				
			||||||
 | 
					              display: false
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            ticks: {
 | 
					            ticks: {
 | 
				
			||||||
              color: 'var(--nord3)',
 | 
					              color: '#ffffff',
 | 
				
			||||||
              font: {
 | 
					              font: {
 | 
				
			||||||
                family: 'Inter, system-ui, sans-serif'
 | 
					                family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
 | 
					                size: 14,
 | 
				
			||||||
 | 
					                weight: 'bold'
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -87,58 +98,74 @@
 | 
				
			|||||||
            stacked: true,
 | 
					            stacked: true,
 | 
				
			||||||
            beginAtZero: true,
 | 
					            beginAtZero: true,
 | 
				
			||||||
            grid: {
 | 
					            grid: {
 | 
				
			||||||
              color: 'var(--nord4)',
 | 
					              display: false
 | 
				
			||||||
              borderDash: [2, 2]
 | 
					            },
 | 
				
			||||||
 | 
					            border: {
 | 
				
			||||||
 | 
					              display: false
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            ticks: {
 | 
					            ticks: {
 | 
				
			||||||
              color: 'var(--nord3)',
 | 
					              color: 'transparent',
 | 
				
			||||||
              font: {
 | 
					              font: {
 | 
				
			||||||
                family: 'Inter, system-ui, sans-serif'
 | 
					                size: 0
 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              callback: function(value) {
 | 
					 | 
				
			||||||
                return 'CHF ' + value.toFixed(0);
 | 
					 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        plugins: {
 | 
					        plugins: {
 | 
				
			||||||
 | 
					          datalabels: {
 | 
				
			||||||
 | 
					            display: false
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          legend: {
 | 
					          legend: {
 | 
				
			||||||
            position: 'bottom',
 | 
					            position: 'bottom',
 | 
				
			||||||
            labels: {
 | 
					            labels: {
 | 
				
			||||||
              padding: 20,
 | 
					              padding: 20,
 | 
				
			||||||
              usePointStyle: true,
 | 
					              usePointStyle: true,
 | 
				
			||||||
              color: 'var(--nord1)',
 | 
					              color: '#ffffff',
 | 
				
			||||||
              font: {
 | 
					              font: {
 | 
				
			||||||
                family: 'Inter, system-ui, sans-serif',
 | 
					                family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
                size: 12
 | 
					                size: 14,
 | 
				
			||||||
 | 
					                weight: 'bold'
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          title: {
 | 
					          title: {
 | 
				
			||||||
            display: !!title,
 | 
					            display: !!title,
 | 
				
			||||||
            text: title,
 | 
					            text: title,
 | 
				
			||||||
            color: 'var(--nord0)',
 | 
					            color: '#ffffff',
 | 
				
			||||||
            font: {
 | 
					            font: {
 | 
				
			||||||
              family: 'Inter, system-ui, sans-serif',
 | 
					              family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
              size: 16,
 | 
					              size: 18,
 | 
				
			||||||
              weight: 'bold'
 | 
					              weight: 'bold'
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            padding: 20
 | 
					            padding: 20
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          tooltip: {
 | 
					          tooltip: {
 | 
				
			||||||
            backgroundColor: 'var(--nord1)',
 | 
					            backgroundColor: '#2e3440',
 | 
				
			||||||
            titleColor: 'var(--nord6)',
 | 
					            titleColor: '#ffffff',
 | 
				
			||||||
            bodyColor: 'var(--nord6)',
 | 
					            bodyColor: '#ffffff',
 | 
				
			||||||
            borderColor: 'var(--nord3)',
 | 
					            borderWidth: 0,
 | 
				
			||||||
            borderWidth: 1,
 | 
					            cornerRadius: 12,
 | 
				
			||||||
            cornerRadius: 8,
 | 
					            padding: 12,
 | 
				
			||||||
 | 
					            displayColors: true,
 | 
				
			||||||
 | 
					            titleAlign: 'center',
 | 
				
			||||||
 | 
					            bodyAlign: 'center',
 | 
				
			||||||
            titleFont: {
 | 
					            titleFont: {
 | 
				
			||||||
              family: 'Inter, system-ui, sans-serif'
 | 
					              family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
 | 
					              size: 13,
 | 
				
			||||||
 | 
					              weight: 'bold'
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bodyFont: {
 | 
					            bodyFont: {
 | 
				
			||||||
              family: 'Inter, system-ui, sans-serif'
 | 
					              family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
 | 
					              size: 14,
 | 
				
			||||||
 | 
					              weight: '500'
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            titleMarginBottom: 8,
 | 
				
			||||||
 | 
					            usePointStyle: true,
 | 
				
			||||||
 | 
					            boxPadding: 6,
 | 
				
			||||||
            callbacks: {
 | 
					            callbacks: {
 | 
				
			||||||
 | 
					              title: function(context) {
 | 
				
			||||||
 | 
					                return '';
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
              label: function(context) {
 | 
					              label: function(context) {
 | 
				
			||||||
                return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
 | 
					                return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
@@ -146,10 +173,53 @@
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        interaction: {
 | 
					        interaction: {
 | 
				
			||||||
          intersect: false,
 | 
					          intersect: true,
 | 
				
			||||||
          mode: 'index'
 | 
					          mode: 'dataset'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      plugins: [{
 | 
				
			||||||
 | 
					        id: 'monthlyTotals',
 | 
				
			||||||
 | 
					        afterDatasetsDraw: function(chart) {
 | 
				
			||||||
 | 
					          const ctx = chart.ctx;
 | 
				
			||||||
 | 
					          const chartArea = chart.chartArea;
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          ctx.save();
 | 
				
			||||||
 | 
					          ctx.font = 'bold 14px Inter, system-ui, sans-serif';
 | 
				
			||||||
 | 
					          ctx.fillStyle = '#ffffff';
 | 
				
			||||||
 | 
					          ctx.textAlign = 'center';
 | 
				
			||||||
 | 
					          ctx.textBaseline = 'bottom';
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Calculate and display monthly totals
 | 
				
			||||||
 | 
					          chart.data.labels.forEach((label, index) => {
 | 
				
			||||||
 | 
					            let total = 0;
 | 
				
			||||||
 | 
					            chart.data.datasets.forEach(dataset => {
 | 
				
			||||||
 | 
					              total += dataset.data[index] || 0;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (total > 0) {
 | 
				
			||||||
 | 
					              // Get the x position for this month from any dataset
 | 
				
			||||||
 | 
					              const meta = chart.getDatasetMeta(0);
 | 
				
			||||||
 | 
					              if (meta && meta.data[index]) {
 | 
				
			||||||
 | 
					                const x = meta.data[index].x;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Find the highest point for this month across all datasets
 | 
				
			||||||
 | 
					                let maxY = chartArea.bottom;
 | 
				
			||||||
 | 
					                for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
 | 
				
			||||||
 | 
					                  const datasetMeta = chart.getDatasetMeta(datasetIndex);
 | 
				
			||||||
 | 
					                  if (datasetMeta && datasetMeta.data[index]) {
 | 
				
			||||||
 | 
					                    maxY = Math.min(maxY, datasetMeta.data[index].y);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Display the total above the bar
 | 
				
			||||||
 | 
					                ctx.fillText(`CHF ${total.toFixed(0)}`, x, maxY - 10);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          ctx.restore();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }]
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -106,9 +106,23 @@ export const GET: RequestHandler = async ({ url, locals }) => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Convert to arrays for Chart.js
 | 
					    // Convert to arrays for Chart.js
 | 
				
			||||||
    const months = Array.from(monthsMap.keys()).sort();
 | 
					    const allMonths = Array.from(monthsMap.keys()).sort();
 | 
				
			||||||
    const categoryList = Array.from(categories).sort();
 | 
					    const categoryList = Array.from(categories).sort();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Find the first month with any data and trim empty months from the start
 | 
				
			||||||
 | 
					    let firstMonthWithData = 0;
 | 
				
			||||||
 | 
					    for (let i = 0; i < allMonths.length; i++) {
 | 
				
			||||||
 | 
					      const monthData = monthsMap.get(allMonths[i]);
 | 
				
			||||||
 | 
					      const hasData = Object.values(monthData).some(value => value > 0);
 | 
				
			||||||
 | 
					      if (hasData) {
 | 
				
			||||||
 | 
					        firstMonthWithData = i;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Trim the months array to start from the first month with data
 | 
				
			||||||
 | 
					    const months = allMonths.slice(firstMonthWithData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const datasets = categoryList.map((category: string) => ({
 | 
					    const datasets = categoryList.map((category: string) => ({
 | 
				
			||||||
      label: category,
 | 
					      label: category,
 | 
				
			||||||
      data: months.map(month => monthsMap.get(month)[category] || 0)
 | 
					      data: months.map(month => monthsMap.get(month)[category] || 0)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -153,6 +153,9 @@
 | 
				
			|||||||
<main class="cospend-main">
 | 
					<main class="cospend-main">
 | 
				
			||||||
    <h1>Cospend</h1>
 | 
					    <h1>Cospend</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Responsive layout for balance and chart -->
 | 
				
			||||||
 | 
					  <div class="dashboard-layout">
 | 
				
			||||||
 | 
					    <div class="balance-section">
 | 
				
			||||||
      <EnhancedBalance bind:this={enhancedBalanceComponent} initialBalance={data.balance} initialDebtData={data.debtData} />
 | 
					      <EnhancedBalance bind:this={enhancedBalanceComponent} initialBalance={data.balance} initialDebtData={data.debtData} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="actions">
 | 
					      <div class="actions">
 | 
				
			||||||
@@ -162,6 +165,7 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <DebtBreakdown bind:this={debtBreakdownComponent} />
 | 
					      <DebtBreakdown bind:this={debtBreakdownComponent} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Monthly Expenses Chart -->
 | 
					    <!-- Monthly Expenses Chart -->
 | 
				
			||||||
    <div class="chart-section">
 | 
					    <div class="chart-section">
 | 
				
			||||||
@@ -181,6 +185,7 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      {/if}
 | 
					      {/if}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {#if loading}
 | 
					  {#if loading}
 | 
				
			||||||
    <div class="loading">Loading recent activity...</div>
 | 
					    <div class="loading">Loading recent activity...</div>
 | 
				
			||||||
@@ -274,7 +279,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
  .cospend-main {
 | 
					  .cospend-main {
 | 
				
			||||||
    max-width: 800px;
 | 
					 | 
				
			||||||
    margin: 0 auto;
 | 
					    margin: 0 auto;
 | 
				
			||||||
    padding: 2rem;
 | 
					    padding: 2rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -355,6 +359,9 @@
 | 
				
			|||||||
    border-radius: 0.75rem;
 | 
					    border-radius: 0.75rem;
 | 
				
			||||||
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
					    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
    border: 1px solid var(--nord4);
 | 
					    border: 1px solid var(--nord4);
 | 
				
			||||||
 | 
					    max-width: 800px;
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					    margin-right: auto;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .recent-activity h2 {
 | 
					  .recent-activity h2 {
 | 
				
			||||||
@@ -705,8 +712,33 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chart-section {
 | 
					  .dashboard-layout {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 2rem;
 | 
				
			||||||
    margin-bottom: 2rem;
 | 
					    margin-bottom: 2rem;
 | 
				
			||||||
 | 
					    max-width: 1400px;
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					    margin-right: auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (min-width: 1200px) {
 | 
				
			||||||
 | 
					    .dashboard-layout {
 | 
				
			||||||
 | 
					      display: grid;
 | 
				
			||||||
 | 
					      grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					      gap: 3rem;
 | 
				
			||||||
 | 
					      align-items: start;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .balance-section {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 2rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chart-section {
 | 
				
			||||||
 | 
					    min-height: 400px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chart-section .loading {
 | 
					  .chart-section .loading {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user