From 7586c78f5501f7dc4626dcee430d365dd0fde43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSamoilenkoVadym=E2=80=9D?= <“samoylenko.vadym@gmail.com”> Date: Sun, 2 Mar 2025 18:56:24 +0000 Subject: [PATCH] Add pie chart and detailed statistics to Dashboard view --- Coinly/Features/Dashboard/DashboardView.swift | 64 ++++++++++++++- Coinly/UI/Components/PieChartView.swift | 80 +++++++++++++++++++ 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 Coinly/UI/Components/PieChartView.swift diff --git a/Coinly/Features/Dashboard/DashboardView.swift b/Coinly/Features/Dashboard/DashboardView.swift index 23037ab..6bb80c3 100644 --- a/Coinly/Features/Dashboard/DashboardView.swift +++ b/Coinly/Features/Dashboard/DashboardView.swift @@ -3,18 +3,34 @@ import SwiftUI struct DashboardView: View { let transactions: [TransactionModel] - var totalBalance: Double { + private var totalBalance: Double { transactions.reduce(0) { $0 + $1.signedAmount } } - var income: Double { + private var income: Double { transactions.filter { !$0.isExpense }.reduce(0) { $0 + $1.amount } } - var expenses: Double { + private var expenses: Double { transactions.filter { $0.isExpense }.reduce(0) { $0 + $1.amount } } + private var categoryExpenses: [PieChartView.PieSlice] { + // Группируем расходы по категориям + let expensesByCategory = Dictionary(grouping: transactions.filter { $0.isExpense }) { $0.category } + let totalExpenses = expenses + + return expensesByCategory.map { category, transactions in + let categoryTotal = transactions.reduce(0) { $0 + $1.amount } + let percentage = totalExpenses > 0 ? categoryTotal / totalExpenses : 0 + return PieChartView.PieSlice( + category: CategoryModel.category(for: category), + amount: categoryTotal, + percentage: percentage + ) + }.sorted { $0.amount > $1.amount } + } + var body: some View { NavigationView { ScrollView { @@ -68,6 +84,44 @@ struct DashboardView: View { .shadow(radius: 2) } + // Expenses by Category + if !categoryExpenses.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Expenses by Category") + .font(.headline) + + // Pie Chart + PieChartView(slices: categoryExpenses) + .frame(height: 200) + .padding(.vertical) + + // Category Legend + VStack(spacing: 12) { + ForEach(categoryExpenses) { slice in + HStack { + Circle() + .fill(Color(slice.category.color)) + .frame(width: 12, height: 12) + + Text(slice.category.name) + + Spacer() + + Text(String(format: "$ %.2f", slice.amount)) + .foregroundColor(.secondary) + + Text(String(format: "(%.1f%%)", slice.percentage * 100)) + .foregroundColor(.secondary) + } + } + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + // Recent Transactions VStack(alignment: .leading, spacing: 12) { Text("Recent Transactions") @@ -75,7 +129,9 @@ struct DashboardView: View { ForEach(Array(transactions.prefix(3))) { transaction in TransactionRowView(transaction: transaction) - Divider() + if transaction.id != transactions.prefix(3).last?.id { + Divider() + } } } .frame(maxWidth: .infinity) diff --git a/Coinly/UI/Components/PieChartView.swift b/Coinly/UI/Components/PieChartView.swift new file mode 100644 index 0000000..a4f4614 --- /dev/null +++ b/Coinly/UI/Components/PieChartView.swift @@ -0,0 +1,80 @@ +// +// PieChartView.swift +// Coinly +// +// Created by Vadym Samoilenko on 02/03/2025. +// + + +import SwiftUI + +struct PieChartView: View { + struct PieSlice: Identifiable { + let id = UUID() + let category: CategoryModel + let amount: Double + let percentage: Double + } + + let slices: [PieSlice] + + var body: some View { + ZStack { + ForEach(slices) { slice in + PieSliceView( + startAngle: startAngle(for: slice), + endAngle: endAngle(for: slice), + color: colorFor(category: slice.category) + ) + } + } + .aspectRatio(1, contentMode: .fit) + } + + private func startAngle(for slice: PieSlice) -> Double { + let index = slices.firstIndex(where: { $0.id == slice.id }) ?? 0 + let precedingSlices = slices.prefix(index) + let precedingPercentages = precedingSlices.map { $0.percentage } + let startPercentage = precedingPercentages.reduce(0, +) + return startPercentage * 360 + } + + private func endAngle(for slice: PieSlice) -> Double { + startAngle(for: slice) + (slice.percentage * 360) + } + + private func colorFor(category: CategoryModel) -> Color { + switch category.color { + case "red": return .red + case "blue": return .blue + case "purple": return .purple + case "orange": return .orange + case "green": return .green + case "gray": return .gray + default: return .gray + } + } +} + +struct PieSliceView: View { + let startAngle: Double + let endAngle: Double + let color: Color + + var body: some View { + GeometryReader { geometry in + Path { path in + let center = CGPoint(x: geometry.size.width/2, y: geometry.size.height/2) + let radius = min(geometry.size.width, geometry.size.height)/2 + path.move(to: center) + path.addArc(center: center, + radius: radius, + startAngle: .degrees(startAngle), + endAngle: .degrees(endAngle), + clockwise: false) + path.closeSubpath() + } + .fill(color) + } + } +} \ No newline at end of file