Area chart with a dimming layer up to the current point in time

In this post we are going to see how to build an area chart that shows UV index throughout the day inspired by the UV index chart in the system iOS Weather app. The chart will also have a line mark filled with a linear gradient to emphasize UV index values, a rule mark with a point mark to show the current time of the day and a dimming layer on the chart area for the time that already passed. Here is what the final result will look like.

Screenshot of an area chart showing UV index in Christchurch throughout the day

# Prepare chart data

To create the chart we'll be using a small demo data set of UV index values recorded in Christchurch on a specific day. We'll have dates on the x-axis and UV index on the y-axis, so we'll map our data to an array of tuples with dates and UV index values. We'll also hardcode the current date to midday. In a real app, the UV index data and the current date would be dynamic.

extension Calendar {
    static func date(bySettingHour hour: Int, of date: Date) -> Date? {
        Calendar.current.date(
            bySettingHour: hour,
            minute: 0,
            second: 0,
            of: date
        )
    }
}

struct ContentView: View {
    let currentDate = Calendar.date(bySettingHour: 12, of: Date())!
    
    let uvData = [
        (hour: 6, uv: 0), (hour: 8, uv: 1),
        (hour: 10, uv: 4), (hour: 12, uv: 6.5),
        (hour: 14, uv: 8.2), (hour: 16, uv: 6),
        (hour: 18, uv: 1.3), (hour: 20, uv: 0)
    ]

    var currentUVData: [(date: Date, uv: Double)] {
        uvData.map {
            (
                date: Calendar.date(
                    bySettingHour: $0.hour, of: currentDate
                )!,
                uv: $0.uv
            )
        }
    }
    
    var body: some View {
        ...
    }
}
Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit

# Build area chart with a line mark

We'll start by looping over our data set and adding an AreaMark to the chart. We'll set the interpolation method to cardinal to get smooth curves and fill the area with a linear gradient to emphasize the UV index value from low to high.

Chart {
    ForEach(currentUVData, id: \.date) { dataPoint in
        AreaMark(
            x: .value("Time of day", dataPoint.date),
            y: .value("UV index", dataPoint.uv)
        )
        .interpolationMethod(.cardinal)
        .foregroundStyle(
            .linearGradient(
                colors: [.green, .yellow, .red],
                startPoint: .bottom, endPoint: .top
            )
            .opacity(0.5)
        )
        .alignsMarkStylesWithPlotArea()
    }
}

Notice that I'm using the alignsMarkStylesWithPlotArea() modifier to make sure that the gradient uses the entire plot area for its start and end points, rather than just following the mark. I talk more about this technique in my previous post Fill bar marks with gradient in Swift Charts.

At this point we'll have an area with a slightly transparent fill that starts with green color for low values and ends with red for the highest ones.

Screenshot of a chart with an area mark filled with a linear gradient

Now we'll add a brighter outline to the chart with a LineMark for the same data. The line mark will also be filled with a linear gradient but without any transparency. While it creates a nice visual effect, Voice Over users don't need to hear the repeated chart data, so we'll hide the area mark from accessibility and only keep the line mark exposed.

Chart {
    ForEach(currentUVData, id: \.date) { dataPoint in
        AreaMark(...)
            .accessibilityHidden(true)
        
        LineMark(
            x: .value("Time of day", dataPoint.date),
            y: .value("UV index", dataPoint.uv)
        )
        .interpolationMethod(.cardinal)
        .foregroundStyle(
            .linearGradient(
                colors: [.green, .yellow, .red],
                startPoint: .bottom, endPoint: .top
            )
        )
        .lineStyle(StrokeStyle(lineWidth: 4))
        .alignsMarkStylesWithPlotArea()
    }
}
Screenshot of a chart with an area mark and a line mark outline

# Configure axis marks

By default the chart only has some mark annotations on the y-axis. I think it would be nicer to show all of the possible values that UV index can take. We'll define our custom axis marks with the desired range.

struct ContentView: View {
    ...

    var body: some View {
        Chart {...}
            .chartYAxis {
                AxisMarks(
                    format: .number,
                    preset: .aligned,
                    values: Array(0...12)
                )
            }
    }
}

Another great thing to add would be the description of the values, so that users know whether the UV index is currently low, moderate, high or extreme. These descriptions will be positioned on the leading edge of the chart and will be inset inside the plot area.

struct ContentView: View {
    ...

    var body: some View {
        Chart {...}
        .chartYAxis {
            AxisMarks(...)
            
            AxisMarks(
                preset: .inset,
                position: .leading,
                values: [1, 3, 6, 8, 11]
            ) { value in
                AxisValueLabel(
                    descriptionForUVIndex(value.as(Double.self)!)
                )
            }
        }
    }
    
    func descriptionForUVIndex(_ index: Double) -> String {
        switch index {
        case 0...2: return "Low"
        case 3...5: return "Moderate"
        case 6...7: return "High"
        case 8...10: return "Very high"
        default: return "Extreme"
        }
    }
}
Screenshot of a chart with y-axis marks going from 0 to 12 and indicating the bounds for low, moderate, high, very high and extreme UV index

# Add a rule mark and a point mark to indicate the current time

To draw the user's attention to the current time and UV index value, we are going to add a RuleMark at the nearest data point to the current time that we have. Since we only need one mark for the entire plot, we'll add it outside of the ForEach but still inside the Chart. We are hiding the rule mark from accessibility because it's present just for visual distinction.

Chart {
    ForEach(currentUVData, id: \.date) { dataPoint in
        ...
    }
    
    if let dataPoint = closestDataPoint(for: currentDate) {
        RuleMark(x: .value("Now", dataPoint.date))
            .foregroundStyle(Color.secondary)
            .accessibilityHidden(true)
    }
}

We'll also add a PointMark to indicate the UV index value for the current time in the day. The point mark will go on top of the rule mark and will have a material background border around it. To get the border, we'll stack two marks on top of each other, but will only expose the top one to accessibility technologies.

Chart {
    ForEach(currentUVData, id: \.date) { dataPoint in
        ...
    }
    
    if let dataPoint = closestDataPoint(for: currentDate) {
        RuleMark(...)
            
        PointMark(
            x: .value("Time of day", dataPoint.date),
            y: .value("UV index", dataPoint.uv)
        )
        .symbolSize(CGSize(width: 16, height: 16))
        .foregroundStyle(.regularMaterial)
        .accessibilityHidden(true)
        
        PointMark(
            x: .value("Time of day", dataPoint.date),
            y: .value("UV index", dataPoint.uv)
        )
        .symbolSize(CGSize(width: 6, height: 6))
        .foregroundStyle(Color.primary)
        .accessibilityLabel("Now")
    }
}
Screenshot of a chart with a rule mark and a point mark indicating the current time and UV index

# Dim the area showing past dates

The final step to complete the chart is to add a dimming layer that covers the area up to the current time. The Weather app chart that we are using for inspiration has dimming that goes over the whole plot, including the axis annotations on the leading edge. I thought it would be better to just cover the area mark and keep annotations prominent independent on where we are in the day.

To achieve the dimming effect, we'll be using a RectangleMark with a material fill and a mask to fit the dimming just on top of the area mark. Let's first add the rectangle. We'll place it below the rule mark and it will span from the start of the area to the current time.

Chart {
    ForEach(currentUVData, id: \.date) { dataPoint in
        ...
    }
    
    if let dataPoint = closestDataPoint(for: currentDate) {
        if let firstDataPoint = currentUVData.first {
            RectangleMark(
                xStart: .value("", firstDataPoint.date),
                xEnd: .value("", dataPoint.date)
            )
            
            .foregroundStyle(.thickMaterial)
            .opacity(0.6)
            .accessibilityHidden(true)
        }
    
        RuleMark(...)
        PointMark(...)
        PointMark(...)
    }
}

Right now we can see that the rectangle covers everything including annotations.

Screenshot of a chart with a rectangle filled with a material background covering the plot area up to the current time

To make it cover just the outlined area mark and keep the rest of the plot area including annotations intact, we'll add a mask in the shape of the marks we already have. Since this time the marks are just used to mask the rectangle and are not actually visible, they don't need any special foreground style. It's important to indicate the series of the marks and set the area mark as unstacked for the chart to be displayed correctly.

RectangleMark(
    xStart: .value("", firstDataPoint.date),
    xEnd: .value("", dataPoint.date)
)

.foregroundStyle(.thickMaterial)
.opacity(0.6)
.accessibilityHidden(true)

.mask {
    ForEach(currentUVData, id: \.date) { dataPoint in
        AreaMark(
            x: .value("Time of day", dataPoint.date),
            y: .value("UV index", dataPoint.uv),
            series: .value("", "mask"),
            stacking: .unstacked
        )
        .interpolationMethod(.cardinal)
        
        LineMark(
            x: .value("Time of day", dataPoint.date),
            y: .value("UV index", dataPoint.uv),
            series: .value("", "mask")
        )
        .interpolationMethod(.cardinal)
        .lineStyle(StrokeStyle(lineWidth: 4))
    }
}
Screenshot of a chart with dimming covering just the area mark up to the current time

We are almost done. To polish the final look of the chart, we'll slightly increase the padding of the plot area, so that the dimming layer covers the small bit of the outline that is drawn below 0 on the y-axis.

Chart {...}
    .chartYScale(range: .plotDimension(padding: 2))
Screenshot of a chart with increased plot area padding

You can get the complete code example for this article from our GitHub repository.

Swift Charts are designed to work with SwiftUI, but you can still use them in a UIKit project. You can check out my recent book Integrating SwiftUI into UIKit Apps for ways to add SwiftUI views to an existing UIKit project to take full advantage of the new iOS 16 APIs.