Skip to content

Swift Charts: Vectorized and function plots

The plot thickens! Learn how to render beautiful charts representing math functions and extensive datasets using function and vectorized plots in your app. Whether you’re looking to display functions common in aerodynamics, magnetism, and higher order field theory, or create large interactive heat maps, Swift Charts has you covered.

Presenter

  • Zhiyu (Apollo) Zhu, SwiftUI Engineer

Key takeaways

  • 🌦️ Present weather trends

  • 🎭 Track mood & vitals

  • 📉 Plot Mathematical Functions

  • 📊 Vectorized Plotting APIs

LinePlot and AreaPlot API

  • LinePlots allow plotting of normal distributions across a graph of data.

  • Accessible by default meaning Voice Over can describe the chart as well as use of Audio Graph

func normalDistribution(_ x: Double, mean: Double, standardDeciation: Double) -> Double {
}
Chart {
LinePlot(
x: "Capacity density", y: "Probability"
) { x in
normalDistribution(
    x,
    mean: mean, 
    standardDeviation: standardDeviation
)
}
.foregroundStyle(.gray)
}
Chart {
AreaPlot(
    x: "Capacity density", y: "Probability"
) { x in
    normalDistribution(
        x,
        mean: mean, 
        standardDeviation: standardDeviation
    )
}
.foregroundStyle(.gray)
.opacity(0.2)
}

AreaPlot can visualize area between functions by returning a tuple of yStart and yEnd for a given input x.

Functions accept an unbound range of x values.

Set Bounds using XScale and YScale. Also you can limit AreaPlot’s domain.

Chart {
    AreaPlot(
        x: "x", yStart: "cos(x)", yEnd: "sin(x)",
        domain: -135...45
    ) { x in
        (yStart: cos(x / 180 * .pi),
        yEnd: sin(x / 180 * .pi))
    }
}
.chartXScale(domain: -315...225)
.chartYScale(domain: -5...5)

Parametric Functions

Parametric fuctions can be graphed using the same LinePlot API

x and y are defined in terms of a 3rd variable, t.

Chart {
    LinePlot(
        x: "x", y: "y", t: "t", domain: -.pi ... .pi
    ) { t in
        let x = sqrt(2) * pow(sin(t), 3)
        let y = cos(t) * (2 - cos(t) - pow(cos(t), 2))
        return (x, y)
    }
}
.chartXScale(domain: -3...3)
.chartYScale(domain: -4...2)

Piecewise functions

Piecewise functions may not have certain values within domain.

Returning .nan informs Swift Charts there’s not a number for that input value x or special values of x

Chart {
    LinePlot(x: "x", y: "x + 1 for x ≥ 0") { x in
        guard x != 0 else {
            return .nan
        }
        return x + 1
    }
}
.chartXScale(domain: -5...10)
.chartYScale(domain: -5...10)
Chart {
    LinePlot(x: "x", y: "1 / x") { x in
        guard x != 0 else {
            return .nan
        }
        return 1 / x
    }
}
.chartXScale(domain: -10...10)
.chartYScale(domain: -10...10)

Extensive Data Visualizations

Scatter plots can visualize a classification model

Heatmaps can visualize a transformer language model’s self-attention

Marks API Review

The flexibility of marks allows you to style each data point differently, though entire collections tend to be styled homogeneously.

Chart {
    ForEach(model.data) {
        if $0.capacityDensity > 0.0001 {
            RectangleMark(
                x: .value("Longitude", $0.x),
                y: .value("Latitude", $0.y)
            )   
            .foregroundStyle(by: .value("Axis type", $0.axisType))
        } else {
            PointMark(
                x: .value("Longitude", $0.x),
                y: .value("Latitude", $0.y)
            )
            .opacity(0.5)
        }
    }
}
Chart {
    ForEach(model.data) {
        RectangleMark(
            x: .value("Longitude", $0.x),
            y: .value("Latitude", $0.y)
        )
        .foregroundStyle(by: .value("Axis type", $0.panelAxisType))
        .opacity($0.capacityDensity)
    }
}

RectanglePlot API Example

Chart {
    RectanglePlot(
        model.data,
        x: .value("Longitude", \.x),
        y: .value("Latitude", \.y)
    )
    .foregroundStyle(by: .value("Axis type", \.panelAxisType))
    .opacity(\.capacityDensity)
}

PointPlot API Example

PointPlots take the same .value syntax. It’s use of KeyPaths allow styling all points without iterating over the dataset. Use of .symbolSize makes points represent capacity.

Chart {
    contiguousUSMap

    PointPlot(
        model.data,
        x: .value("Longitude", \.x),
        y: .value("Latitude", \.y)
    )
    .symbolSize(by: .value("Capacity", \.capacity))
    .foregroundStyle(
        by: .value("Axis type", \.panelAxisType)
    )
}

Model definition

@Observable class Model {
    var data: [DataPoint]
}

Stored Properties allows Swift Charts to access the x and y values for all data points with a constant memory offset instead of calling the getter for every data point.

struct DataPoint: Identifiable {
    let id: Int

    let: capacity: Double
    let: panelAxisType: String

    let: xLongitude: Double
    let: yLatitude: Double

    // Albers projection
    var x: Double
    var y: Double
}

Chart Modifiers

  • foregroundStyle

  • opacity

  • symbol

  • symbolSize

  • lineStyle

  • accessibilityLabel

  • accessibilityValue

  • accessibilityIdentifier

  • position

  • accessibilityHidden

When to Use

  • Use Vectorized Plots for larger datasets where the entire plot is customized with the same modifiers and properties.

  • Use the Mark API when you have fewer data points, but need to customize each element with individual mark types and modifiers, or if you need complex layering with zIndex.

Vectorized Plot Performance

  • Group data by style

  • Avoid computed properties

  • Specify scale domains if known

  • Avoid unnecessary styling

Missing anything? Corrections? Contributions are welcome!

Written By

dl-alexandre
dl-alexandre
5 notes contributed