skip to Main Content

I can draw a bar chart using Swift Charts framework.
Now I want to perform 2 actions in below bar chart.

  1. Action on tap on green bar
  2. Action on tap on X axis red highlighted rectangle area with label text

I have used tap gesture but it’s not working on tapping point 2 area.

import SwiftUI
import Charts

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

struct TapChart: View {

    let items: [Item] = [
        Item(month: "Jan", revenue: 100, visits: 2),
        Item(month: "Feb", revenue: 200, visits: 1),
        Item(month: "Mar", revenue: 150, visits: 4),
        Item(month: "Apr", revenue: 130, visits: 0),
        Item(month: "May", revenue: 300, visits: 3),
        Item(month: "Jun", revenue: 50, visits: 2)
    ]

    var myItems = [Item]()

    var body: some View {
        Chart {
            ForEach(items) { items in
                BarMark(
                    x: .value("Month", items.month),
                    y: .value("Revenue", items.revenue)
                )
                .foregroundStyle(Color.green.gradient)
            }
        }
        .chartForegroundStyleScale([
            "SEO Revenue" : Color(.green),
            "Visits": Color(.purple)
        ])
        .chartLegend(position: .top, alignment: .bottomTrailing)
        .chartXAxis {
            AxisMarks { axis in
                AxisValueLabel(centered: true) {
                    Text("(items[axis.index].month)")
                        .font(.system(size: 12, weight: .bold))
                        .padding(.horizontal, 12)
                        .padding(.vertical, 8)
                        .background(Color.red.opacity(0.3))
                        .cornerRadius(4)
                        .onTapGesture {
                            print("(items[axis.index].month)")
                        }
                }
            }
        }
        .chartYAxis(.visible)
        .padding()
        .cornerRadius(15)
        .frame(height: 300)
    }
}

Thanks in advance.

enter image description here

2

Answers


  1. I think you would have to reinvent your own axis marks with a chartOverlay

    .chartXAxis(.hidden)
    .chartOverlay { proxy in
        if let y = proxy.position(forY: 0.0) {
            ForEach(items) { item in
                if let x = proxy.position(forX: item.month) {
                    Text("(item.month)")
                        .font(.system(size: 12, weight: .bold))
                        .padding(.horizontal, 12)
                        .padding(.vertical, 8)
                        .background(Color.red.opacity(0.3))
                        .cornerRadius(4)
                        .position(x: x, y: y + 40) // +40 to offset the label downwards a bit
                        .onTapGesture {
                            print("Tapped on label (item.month)")
                        }
                }
            }
        }
    }
    

    To prevent the labels to be clipped by the plot area, you should also add some bottom padding:

    .chartPlotStyle { plot in
        plot.padding(.bottom, 30)
    }
    

    As for detecting taps on the bars, you can do:

    .chartGesture{ proxy in
        SpatialTapGesture().onEnded { value in
            guard let x = proxy.value(atX: value.location.x, as: String.self),
                  let xRange = proxy.positionRange(forX: x) else { return }
            let rangeWidth = xRange.upperBound - xRange.lowerBound
    
            // assuming the bars occupy half of the available width
            // i.e. BarMark(x: ..., y: ..., width: .ratio(0.5))
            let barRatio = 0.5
            let barRange = (xRange.lowerBound + rangeWidth * barRatio / 2)...(xRange.upperBound - rangeWidth * barRatio / 2)
            
            guard barRange.contains(value.location.x),
                  let item = items.first(where: { $0.month == x }),
                  let y = proxy.position(forY: item.revenue),
                  let maxY = proxy.position(forY: 0.0),
                  value.location.y >= y && value.location.y <= maxY else {
                return
            }
            print("Tapped on bar (x)")
            
        }
    }
    
    Login or Signup to reply.
  2. In iOS 17, Apple introduced a native way to handle selections in charts using chartXSelection. This makes it easier to create interactive visualizations without custom handling.

    import SwiftUI
    import Charts
    
    struct ChartExample: View {
        @State private var barSelection: String?
    
        var body: some View {
            Chart {
                ForEach(data) { shape in
                    BarMark(
                        x: .value("Month", shape.month),
                        y: .value("Play Count", shape.playCount)
                    )
                    .cornerRadius(4)
                    .foregroundStyle(Color(cgColor: colour3))
                }
                
                // Add a rule mark to highlight the selected bar
                if let barSelection {
                    RuleMark(x: .value("Month", barSelection))
                        .foregroundStyle(.green.opacity(0.5))
                        .zIndex(-15)
                        .offset(yStart: -15)
                        .annotation(position: .top,
                                    spacing: 0,
                                    overflowResolution: .init(x: .disabled, y: .disabled)) {
                            VStack {
                                Text(barSelection)
                            }
                        }
                }
            }
            .chartXSelection(value: $barSelection)
        }
    }
    
    // Example data
    struct ToyShape: Identifiable {
        var id = UUID()
        var month: String
        var playCount: Int
    }
    
    let data = [
        ToyShape(month: "January", playCount: 10),
        ToyShape(month: "February", playCount: 20),
        ToyShape(month: "March", playCount: 15)
    ]
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search