UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Changing the colour and annotation for a specifc barmark on a chart

Forums > SwiftUI

Hello. I have the working chart below that shows 'revenue' and 'visits' as two seperate barmarks in a chart :) I would like to customize each bar though. i.e. I would like to add a Gradient to each of 'revenue' and 'visits', i'd also like to annotate them with the value represented by the barmark. I think I need to do somethibg with chartForegroundStyleScale, but i'm unsure of the syntax. Any ideas.


import SwiftUI
import Charts

struct Item: Identifiable {
    let id = UUID()
    let month: String
    let revenue: Int
    let visits: Int
}

var myItems = [Item]()
struct largeRevenueVisitsChart: View {

    let currencyDisplay = Locale.current.currencySymbol

    var body: some View {

        let items:[Item] = [
                  Item( month: eAData.eaMonthName[12],
                  revenue: eAData.eaRevenue[12],
                  visits: eAData.eaVisits[12]),

                  Item( month: eAData.eaMonthName[11],
                  revenue: eAData.eaRevenue[11],
                  visits: eAData.eaVisits[11]),

                  Item( month: eAData.eaMonthName[10],
                  revenue: eAData.eaRevenue[10],
                  visits: eAData.eaVisits[10]),

                  Item( month: eAData.eaMonthName[9],
                  revenue: eAData.eaRevenue[9],
                  visits: eAData.eaVisits[9]),

                  Item( month: eAData.eaMonthName[8],
                  revenue: eAData.eaRevenue[8],
                  visits: eAData.eaVisits[8]),

                  Item( month: eAData.eaMonthName[7],
                  revenue: eAData.eaRevenue[7],
                  visits: eAData.eaVisits[7]),

                  Item( month: eAData.eaMonthName[6],
                  revenue: eAData.eaRevenue[6],
                  visits: eAData.eaVisits[6]),

                  Item( month: eAData.eaMonthName[5],
                  revenue: eAData.eaRevenue[5],
                  visits: eAData.eaVisits[5]),

                  Item( month: eAData.eaMonthName[4],
                  revenue: eAData.eaRevenue[4],
                  visits: eAData.eaVisits[4]),

                  Item( month: eAData.eaMonthName[3],
                  revenue: eAData.eaRevenue[3],
                  visits: eAData.eaVisits[3]),

                  Item( month: eAData.eaMonthName[2],
                  revenue: eAData.eaRevenue[2],
                  visits: eAData.eaVisits[2]),

                  Item( month: eAData.eaMonthName[1],
                  revenue: eAData.eaRevenue[1],
                  visits: eAData.eaVisits[1])
                  ]

        let stepData = [
            (period: "Revenue", data: items.map { (month: $0.month, value: $0.revenue) }),
            (period: "Visits", data: items.map { (month: $0.month, value: $0.visits) })
        ]

        NavigationView {
            VStack() {
                GroupBox ("Monthly Revenue & Visits") {
                    Chart (stepData, id: \.period) { steps in
                        ForEach(steps.data, id: \.self.month) {
                            BarMark(
                                x: .value("Visits", $0.value),
                                y: .value("Month", $0.month),
                                width: 15
                            )
                            .foregroundStyle(by: .value("Value", steps.period))
                            .opacity(mobVariables.mobOpacity)
                            .cornerRadius(10)
                            .position(by: .value("Month", steps.period))
                        }
                    }
                    .chartForegroundStyleScale([
                        "Revenue" : Color(.green),
                        "Visits": Color(.purple)])

                    .chartLegend(position: .top, alignment: .bottomTrailing)
                    .chartYAxis(.visible)
                }
            }
        }
    }
}

1      

The Bar Gradient

.chartForegroundStyleScale(["Revenue": LinearGradient(gradient: Gradient(colors: [.yellow, .black]), startPoint: .top, endPoint: .bottom), "Visits": LinearGradient(gradient: Gradient(colors: [.pink, .blue]), startPoint: .top, endPoint: .bottom)])

The

.annotation(position: .top) {
    Text("\(data.amount / 1000)") 
        .font(.caption)
}

Here my project

import Charts
import SwiftUI

struct ContentView: View {
    let items: [Item] = [
        Item(month: "Jan", revenue: 20000, visits: 5),
        Item(month: "Feb", revenue: 25000, visits: 10),
        Item(month: "Mar", revenue: 15000, visits: 7),
        Item(month: "Apr", revenue: 10000, visits: 25),
        Item(month: "May", revenue: 30000, visits: 5),
        Item(month: "Jun", revenue: 40000, visits: 8),
        Item(month: "Jul", revenue: 2000, visits: 10),
        Item(month: "Sep", revenue: 7000, visits: 10),
        Item(month: "Oct", revenue: 12000, visits: 12),
        Item(month: "Nov", revenue: 8000, visits: 13),
        Item(month: "Dec", revenue: 17000, visits: 13)
    ]

    var body: some View {
        let stepData = [
                    (type: "Revenue", data: items.map { (month: $0.month, amount: $0.revenue) }),
                    (type: "Visits", data: items.map { (month: $0.month, amount: $0.visits * 1000) })
                ]

        Chart(stepData, id: \.type) { steps in
            ForEach(steps.data, id: \.month) { data in // <- have to add this as the annotation has different parameters
                BarMark(
                    x: .value("Month", data.month),
                    y: .value("Visits", data.amount), width: 10
                )
                .foregroundStyle(by: .value("Value", steps.type))
                .position(by: .value("Month", steps.type), axis: .horizontal)
                .annotation(position: .top) {
                    Text("\(data.amount / 1000)")
                        .font(.caption)
                }
            }
        }
        .chartForegroundStyleScale(["Revenue": LinearGradient(gradient: Gradient(colors: [.yellow, .purple]), startPoint: .top, endPoint: .bottom), "Visits": LinearGradient(gradient: Gradient(colors: [.pink, .blue]), startPoint: .top, endPoint: .bottom)])
        .chartYAxis(.hidden)
        .frame(height: 300)
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct Item: Identifiable {
    let id = UUID()
    let month: String
    let revenue: Int
    let visits: Int
}

PS change your value to amount just as getting confused between value in charts and data.

1      

SOLVED! The Amazing @NigelGee strikes again. I can't thank you enough, i've learned so much from your answers here on HWS. I don't know if you're involved in the HWS business but thanks to your answers I think it's about time to get a subscription.

1      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Thank you for the question @flaneur7508. I have any done a little bit with the new chart API, so it was a learning curve for me. As Paul said in recent conference “ Everyone has something to learn. Everyone has something to teach.”.

I would hide the legend and put the Chart in a VStack and by adding .chartLegend(.hidden) to the chart. Then add this these computed proprties

var revenueGradient: LinearGradient {
  LinearGradient(gradient: Gradient(colors: [.yellow, .purple]), startPoint: .top, endPoint: .bottom)
}

var visitsGradient: LinearGradient {
  LinearGradient(gradient: Gradient(colors: [.pink, .blue]), startPoint: .top, endPoint: .bottom)
}

and the add this to after the Chart

HStack {
    Circle()
        .fill(revenueGradient)
        .frame(width: 15)
    Text("Revenue (1000)")

    Circle()
        .fill(visitsGradient)
        .frame(width: 15)

    Text("Visits")
}
.font(.caption)
.foregroundColor(.secondary)

PS now you can change the .chartForegroundStyleScale to

.chartForegroundStyleScale(["Revenue": revenueGradient, "Visits": visitsGradient])

so when you want to change the gradient you only have to do in one place

PS if you look at the bottom of the post you see a heart, a checkmark and a link. If you click the check mark on the post that solve it then will display green border and put SOLVED it the title

1      

Thanks, i'll take a look at these. If I may i'd like to add another question to this thread, it could also be a useful for other readers. I've swapped the X and Y axis because the chart is displayed in a sheet in portrait mode. As you can tell its displaying visits which is a simple integer and revenue that's and integer but I need a currency symbol on the annotation.

I can"t see how I can have one axis (visits) without a currency symbol and revenue with a currency symbol. I've been messaging with both .annotation and .chartForegroundStyleScale but no success so far.

And the second and final question concerns the X axis the value of Revenue can be in the millions. The X axis starts at 0, then has points 10E6, 20E6. I simply want an x axis that starts at zero and end at the higest value barmark.

Any ideas?

1      

.annotation is only for the value you show in your chart. When you want changes to the axis you have to use either or both .chartXAxis {} or .chartYAxis {}.

In both closures you can use AxisMarks() and in that AxisValueLabel() and AxisGridLine(). With AxisValueLabel you can format the label on the axis.

1      

Hi @Hatsushira. Maybe I was not clear in my description. The annotation I refer to is including a value against a barmark in a bar chart. I wish I could post an image here but I think you see what I mean. So I have 12 months, for each month a bar showing Revenue and one showing Visits. I want a value with a currency symbol against the Revenue bar but not the visits. Maybe the following help..

                BarMark(
                    x: .value("Month", data.month),
                    y: .value("Visits", data.amount), width: 10
                )
                .foregroundStyle(by: .value("Value", steps.type))
                .position(by: .value("Month", steps.type), axis: .horizontal)
                .annotation(position: .top) {
                    Text("\(data.amount / 1000)")
                        .font(.caption)
                }
            }

1      

Ah, now I get it.

You could use a if statement in your annotation and check for the type. I'm currently not at Xcode so I can't try it myself.

For your question about the value range you should be able to use. .chartXScale(domain: 0...yourMaxValue)

1      

@Hatsushira. Hi. Yes that what I thought but i'm unsure what condition I would use to determine if the barmark is Revenue or Visit.

I'll look into .chartXScale(domain: 0...yourMaxValue). Thank you. All of this is pretty new and i've not come across this modifier until now :)

1      

You have a type property you use for grouping. This should give you the option to decide to show the currency symbol.

Pseudocode here for your annotation:

if type == revenue {
 Text format with currency
 } else {
  Text visit
 }

For me this is all new, too. Have you seen Sean Allen's SwiftUI Bar Chart with Customizations

1      

Hi @flaneur7508

The Chart y axis is months not Vsits/Revenue and the x axis is the scale of the maximum amount number. This is why in my example i used the magic number of 1000 to make the two bars be in range of each other.

So i replaced the 1000 with a multiply

var multiply: Int {
    let maxRevenue = items.map { $0.revenue }.max() ?? 10
    if maxRevenue > 1_000_000 {
        return 1_000_000
    } else if maxRevenue > 100_000 {
        return 100_000
    }

    return 1000
}

With regards the annotation you can do this

.annotation(position: .trailing) { // <- change to .trailing as change in derection of chart
    if steps.type == "Revenue" {
        Text("$\(data.amount / multiply)") // <- see below to change thr currency symbol
            .font(.caption)
    } else {
        Text("\(data.amount / multiply)")
            .font(.caption)
    }
}

PS found the you can customise the legand with this

.chartLegend {
HStack {
    Circle()
        .fill(revenueGradient)
        .frame(width: 15)
    Text("Revenue (\(multiply))")

    Circle()
        .fill(visitsGradient)
        .frame(width: 15)

    Text("Visits")
}
.font(.caption)

So while project

import Charts
import SwiftUI

struct ContentView: View {
    let items: [Item] = [
        Item(month: "Jan", revenue: 20000, visits: 5),
        Item(month: "Feb", revenue: 25000, visits: 10),
        Item(month: "Mar", revenue: 15000, visits: 7),
        Item(month: "Apr", revenue: 10000, visits: 25),
        Item(month: "May", revenue: 30000, visits: 5),
        Item(month: "Jun", revenue: 40000, visits: 8),
        Item(month: "Jul", revenue: 2000, visits: 10),
        Item(month: "Sep", revenue: 7000, visits: 10),
        Item(month: "Oct", revenue: 12000, visits: 12),
        Item(month: "Nov", revenue: 8000, visits: 13),
        Item(month: "Dec", revenue: 17000, visits: 13)
    ]

    let currencyDisplay = Locale.current.currencySymbol

    var multiply: Int {
        let maxRevenue = items.map { $0.revenue }.max() ?? 10
        if maxRevenue > 1_000_000 {
            return 1_000_000
        } else if maxRevenue > 100_000 {
            return 100_000
        }

        return 1000
    }

    var revenueGradient: LinearGradient {
        LinearGradient(gradient: Gradient(colors: [.yellow, .purple]), startPoint: .trailing, endPoint: .leading)
    }

    var visitsGradient: LinearGradient {
        LinearGradient(gradient: Gradient(colors: [.pink, .blue]), startPoint: .trailing, endPoint: .leading)
    }

    var body: some View {
        let stepData = [
                    (type: "Revenue", data: items.map { (month: $0.month, amount: $0.revenue) }),
                    (type: "Visits", data: items.map { (month: $0.month, amount: $0.visits * multiply) })
                ]
        VStack {
            Chart(stepData, id: \.type) { steps in
                ForEach(steps.data, id: \.month) { data in
                    BarMark(
                        x:.value("Visits", data.amount),
                        y: .value("Month", data.month), width: 10
                    )

                    .foregroundStyle(by: .value("Value", steps.type))
                    .position(by: .value("Month", steps.type), axis: .vertical)
                    .annotation(position: .trailing) {
                        if steps.type == "Revenue" {
                            Text("\(currencyDisplay ?? "$")\(data.amount / multiply)")
                                .font(.caption)
                        } else {
                            Text("\(data.amount / multiply)")
                                .font(.caption)
                        }
                    }
                }
            }
            .chartForegroundStyleScale(["Revenue": revenueGradient, "Visits": visitsGradient])
            .chartLegend {
                HStack {
                    Circle()
                        .fill(revenueGradient)
                        .frame(width: 15)
                    Text("Revenue (\(multiply))")

                    Circle()
                        .fill(visitsGradient)
                        .frame(width: 15)

                    Text("Visits")
                }
                .font(.caption)
            }
            .chartXAxis(.hidden)
            .padding()
        }
    }
}

PS preview timeout I think it because of the Gradient but does run in simulator. Screenshot

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.