# Interactive Charts in SwiftUI

After the post about Charts in SwiftUI, some readers asked me how we can show the numbers in the chart when tapped. Today, we’re going to create a more interactive chart. We’re going to reuse code from [this post](https://xavier7t.com/charts-in-swiftui), go check it if you haven’t.

The source code of today’s post is available [here](https://github.com/xavier7t/DemoInteractiveChart20230212).

# Step 0 - Preparation

In this demo, we’ll be using the same model and data array from the previous post. So let’s copy the code and paste it below the preview struct of `ContentView`.

```swift
import SwiftUI

struct ContentView: View {...}

struct ContentView_Previews: PreviewProvider {...}

//MARK: Data Source
//Model
struct SalesRecord: Identifiable {
    let id = UUID()
    let office: String
    let salesVolume: Int
}

//Data
let salesRecords: [SalesRecord] = [
    .init(office: "New York", salesVolume: 3000),
    .init(office: "San Fran", salesVolume: 2750),
    .init(office: "California", salesVolume: 4020),
    .init(office: "Denver", salesVolume: 930),
    .init(office: "Kansas", salesVolume: 2100)
]
```

Import Charts and then add the Chart with an Area Mark from the previous post. Let’s add a frame modifier to the Chart element with a height of 200.

```swift
import SwiftUI
import Charts

struct ContentView: View {
    var body: some View {
        NavigationStack {
            VStack {
                Chart {
                    ForEach(salesRecords) { record in
                        AreaMark(
                            x: .value("Office Location", record.office),
                            y: .value("Sales Volume", record.salesVolume)
                        )
                        .foregroundStyle(.linearGradient(colors: [.cyan, .cyan.opacity(0.2)], startPoint: .top, endPoint: .bottom))
                        .foregroundStyle(.pink.opacity(0.7))
                    }
                }
                .frame(height: 200)
                .padding()
                .navigationTitle("Sales Report")
            }
        }
    }
}
```

# Step 1 - Add a Rule mark

Add a Rule mark after the Area mark to indicate the sales volume (the value of Y-axis). Give it a foreground style of `.pink` and for now we’ll keep the plottable value constant at 1200.

```swift
RuleMark(y: .value("Sales", 1200))
    .foregroundStyle(.pink)
```

You should see a pink horizontal rule mark in your preview.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1676236978411/c2bff6e2-f9d1-491b-b0cd-fd931b341fd1.png align="center")

# Step 2 - Add a State and make the Rule mark conditional

Since we only want the rule mark to show up when the plot area is touched, so we need to add a State property to let the view hold the selected value - a pair of office location and sales, therefore let’s add the line below:

```swift
struct ContentView: View {
    @State private var selectedRecord: (String, Int)? = nil
    //...
}
```

And we need the rule mark only when `selectedRecord` is not nil, so we need to put the rule mark inside a `if` statement where we check if `selectedRecord` is nil.

```swift
if let selectedRecord {
    if (selectedRecord.1 > 0 && selectedRecord.1 < 5000) {
            RuleMark(y: .value("Sales", 1200))
                .foregroundStyle(.pink)
    }
}
```

Now the Rule Mark should disappear since the state property `selectedRecord` is set to nil.

# Step 3 - Update State according to gesture location

Now we need to let the view know when and where to show the rule mark.

But first, we need to know the location of the selected point at the chart and convert it to a CGPoint for the view. This is where we need `chartOverlay` and `GeometryReader` .

`GeometryReader` is a SwiftUI View that can help us determine the location (in x,y coordinates) of users selection here. and `chartOverlay` is a modifer for Chart elements, that allows us to map the location to chart values.

```swift
.frame(height: 200) // done previously
//-------new line: added chartOverlay
.chartOverlay { proxy in
    GeometryReader { geometry in
        Rectangle().fill(.clear).contentShape(Rectangle())
    }
}
```

The code above added a clear Rectangle above the chart (you can change `.clear` to `.red` temporarily to see it in the preview). Now we’ll add a `.gesture` modifier with a `DragGesture` for the Rectangle.

```swift
Rectangle().fill(.clear).contentShape(Rectangle())
    .gesture(DragGesture()
        .onChanged { value in
            // get plot area coordiate from gesture location
            let origin = geometry[proxy.plotAreaFrame].origin
            let location = CGPoint(
                x: value.location.x - origin.x,
                y: value.location.y - origin.y)
            print(location)
            // update state
            selectedRecord = proxy.value(at: location, as: ((String, Int).self))
        }
        .onEnded{ _ in
            // reset state when gesture ends
            selectedRecord = nil
        }
    )
```

This gesture might be confusing but don’t worry, let’s get it clear line by line.

**OnChanged:**

Let’s look at the image below: we know the location of the point (x,y) and the origin (0,0), and it’s clear that the differences between these two points are `x` ( = x - 0) on the x-axis and `y` (y - 0) on the y-axis. This is based on the origin being (0,0).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1676238032307/48b11aec-89e3-41cb-acb7-8cf4f315ab08.png align="center")

*Selected point*

Now, we don’t know the coordinates of the origin or the selected point but the geometry reader can provide us with the information. The local variable `value` in the `onChanged` closure is the value pressed on the chart overlay rectangle and we can get its location with `value.location`, which is a pair of CGPoint values. That being said, the coordinate of the selected point is (value.location.x, value.location.y).

*Origin*

`let origin = geometry[proxy.plotAreaFrame].origin` This line returns another pair of CGPoints, which represents the origin of the plot area of our chart.

*Location of the selected point*

Like we can know the point coordinate (x, y) with (x - 0, y- 0), now we can know that the location of the selected point is:

* x = value.location.x - origin.x
    
* y = value.location.y - origin.y
    

*Plot*

Then we can use this location to get the respective point in the chart plot area, with `proxy.value` . This function requires two parameters:

1. at: a CGPoint, here we’ll pass in the location of our selected point;
    
2. as: the type of plot value, which is the type of a pair of office and sales volume, hence `(String, Int).self`
    

**On Ended:**

Whenever the users’ finger leaves the screen, we need to reset our state. So in the on ended closure, we can set the state value back to nil in order to hide the rule mark again.

# Step 4 - Update the Rule Mark

You might notice that now when you’re dragging upon the chart, the rule mark will show up but not move with your finger/cursor. This is because we make the RuleMark’s value constant as 1200 during Step 1.

Now let’s update the RuleMark by updating the plot value from `1200` to `selectedRecord.1` . Since `selectedRecord` is a tuple, `selectedRecord.0` represents the office location string and `selectedRecord.1` is the integer of sales volume.

```swift
if let selectedRecord {
    if (selectedRecord.1 > 0 && selectedRecord.1 < 5000) {
        RuleMark(y: .value("Sales", selectedRecord.1))
            .foregroundStyle(.pink)
    }
}
```

We’re 99% done, but just one more step: to present the value of the y-axis upon the rule mark - add a `.annotation` modifier and for its content, add a text of `selectedRecord.1` to show the numbers too.

```swift
RuleMark(y: .value("Sales", selectedRecord.1))
    .foregroundStyle(.pink)
    .annotation {
        Text("\(selectedRecord.1)")
            .foregroundColor(.secondary)
    }
```

That’s all for today’s post. Hope you understood how the Overlay and Geometry Reader work. If you have any questions, please leave a comment. Remember to subscribe to my newsletter to get more posts like this. See you all tomorrow!
