Search code examples
swiftswiftuiswiftui-charts

How to show multiple data in one month using SwiftUI's Charts library in a bar chart?


I'm working on a personal app and I want to update the code. Currently I'm using an external cocoapod's library named "Charts" to create my charts in my app (which is in Swift). However, I could notice that I can Implement SwiftUI's native "Charts" library and implement it in my code, but I'm not proficient on SwiftUI yet. I'm implementing different sources to achieve this (like this youtube tutorial and this other tutorial). Nevertheless, I would like to show 3 values per month like I show in the image called "01_3BarsPerMonth" wantedResult (which was created using the "Charts" cocoapod library in the original project), but the closest thing I have achieved is as the one shown in the 2nd link I provided you (see image "02_CurrentResult"currentResult) which is currently made with the SwiftUI native "Charts" library. I have searched other examples but they have more (and complex) code that for now, I don't need and I believe that with what I have in this moment, I might get the results I want.

In the test code I'm using now (a small practice app in order to get familiar with SwiftUI's "Charts" library), this is my ViewController class:

import SwiftUI


class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    
        setupView()
    }


// MARK: - Setup view
private func setupView() {
    view.backgroundColor = .lightGray
    
    let horizontalConstraint: CGFloat = 10
    //let controller = UIHostingController(rootView: SavingsHistory())
    let controller = UIHostingController(rootView: OrdersHistory())
    
    guard let savingsView = controller.view else { return }
    
    savingsView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(savingsView)
    
    NSLayoutConstraint.activate([
        savingsView.topAnchor.constraint(equalTo: view.topAnchor),
        savingsView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        savingsView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: horizontalConstraint),
        savingsView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: -horizontalConstraint)
    ])
}
}

And this is the code of my struct:

import SwiftUI
import Charts


struct Order: Identifiable {
    var id: String = UUID().uuidString
    var amount: Int
    var day: Int
}


struct OrdersHistory: View {
    var ordersWeekOne: [Order] = [
        Order(amount: 10, day: 1),
        Order(amount: 7, day: 2),
        Order(amount: 4, day: 3),
        Order(amount: 13, day: 4),
        Order(amount: 19, day: 5),
        Order(amount: 6, day: 6),
        Order(amount: 16, day: 7)
    ]
    var ordersWeekTwo: [Order] = [
        Order(amount: 20, day: 1),
        Order(amount: 14, day: 2),
        Order(amount: 8, day: 3),
        Order(amount: 26, day: 4),
        Order(amount: 27, day: 5),
        Order(amount: 12, day: 6),
        Order(amount: 32, day: 7)
    ]
    var ordersWeekThree: [Order] = [
        Order(amount: 5, day: 1),
        Order(amount: 3, day: 2),
        Order(amount: 2, day: 3),
        Order(amount: 7, day: 4),
        Order(amount: 10, day: 5),
        Order(amount: 3, day: 6),
        Order(amount: 8, day: 7)
    ]

var body: some View {
    Chart {
        ForEach(ordersWeekOne, id: \.id) { order in
            LineMark(
                x: PlottableValue.value("Week 1", order.day),
                y: PlottableValue.value("Orders 1", order.amount),
                series: .value("Week", "One")
            )
            .foregroundStyle(Color.red)
        }
        
        ForEach(ordersWeekTwo, id: \.id) { order in
            LineMark(
                x: PlottableValue.value("Week 2", order.day),
                y: PlottableValue.value("Orders 2", order.amount)
                ,
                series: .value("Week", "Two")
            )
            .foregroundStyle(Color.green)
        }
        
        ForEach(ordersWeekThree, id: \.id) { order in
            LineMark(
                x: PlottableValue.value("Week 3", order.day),
                y: PlottableValue.value("Orders 3", order.amount)
                ,
                series: .value("Week", "Three")
            )
            .foregroundStyle(Color.black)
        }
    }
}
}

I've tried modifying the example above but I always get an error complaint such as "[OrdersHistory] must conform to Identifiable" or something like that when I try to create an object of type [[OrdersHistory]].

What can I modify to get a result as in the first image (3 values for the month of January, for instance).

I appreciate your feedback. Kind regards.


Solution

  • I would start by refactoring your Order type to include the week number

    struct Order: Identifiable {
        var id: String = UUID().uuidString
        var week: Int
        var amount: Int
        var day: Int
    }
    

    Then you can have a single array as the data source

    var orders: [Order] = [
        Order(week: 1, amount: 10, day: 1),
        Order(week: 1, amount: 7, day: 2),
        ...
        Order(week: 3, amount: 3, day: 6),
        Order(week: 3, amount: 8, day: 7)
    ]
    

    Then we need to use strings for our labels/groups instead of Int

    extension Order {
        var weekLabel: String {
            "\(week)"
        }
    
        var dayLabel: String {
            Calendar.current.weekdaySymbols[day-1]
        }
    }
    

    And now we can use those labels and the array for the chart

    Chart {
        ForEach(orders) { order in
            BarMark(
                x: .value("Day", order.dayLabel),
                y: .value("Amount", order.amount)
            )
            .foregroundStyle(by: .value("Week", order.weekLabel))
            .position(by: .value("Week", order.weekLabel))
        }
    
    }
    

    If you want to work with the 3 different arrays you can join them into an array of arrays

    let ordersWeek: [[Order]] = [
        [
            Order(amount: 10, day: 1),
            Order(amount: 7, day: 2),
            //...
        ], [
            Order(amount: 20, day: 1),
            Order(amount: 14, day: 2),
            //...
        ], [
            Order(amount: 5, day: 1),
            Order(amount: 3, day: 2),
            //...
        ]
    ]
    

    And then use stacking as suggested by @Sweeper

    Chart {
        ForEach(ordersWeek.indices, id: \.self) { index in
            ForEach(ordersWeek[index]) { order in
                BarMark(
                    x: .value("Day", order.dayLabel),
                    y: .value("Amount", order.amount),
                    stacking: MarkStackingMethod.standard)
                .foregroundStyle(by: .value("Week", "\(index + 1)"))
                .position(by: .value("Week", "\(index)"))
            }
        }
    }
    

    An alternative way to creating the array of arrays from the original array properties is to use a computed property:

    var ordersWeek: [[Order]] {
        [ordersWeekOne, ordersWeekTwo, ordersWeekThree]
    }