Skip to content

0x073-SwifUI-Charts笔记2

Warning

This article is extracted from the chat log with AI. Please identify it with caution.

我们将从基础的柱状图(Bar Chart)扩展到更复杂的图表类型和自定义功能。以下是学习目标:

  1. 学习不同类型的图表:比如折线图(Line Chart)、面积图(Area Chart)等。
  2. 自定义图表样式:调整颜色、标注、网格线等。
  3. 处理动态数据:从数组或模型中动态生成图表数据。
  4. 交互功能:添加用户交互,比如点击图表显示详细信息。

步骤 1:不同类型的图表

SwiftUI Charts 支持多种图表类型,我们可以用同一个数据集展示不同的可视化效果。这里我们将身高数据从柱状图改为折线图(Line Chart)。

示例代码:折线图

swift
Chart {
	LineMark(
		x: .value("Name", "Alice"),
		y: .value("Height (cm)", 165)
	)
	LineMark(
		x: .value("Name", "Bob"),
		y: .value("Height (cm)", 180)
	)
	LineMark(
		x: .value("Name", "Charlie"),
		y: .value("Height (cm)", 172)
	)
}
.chartYAxisLabel("Height (cm)") // 设置纵轴标题
.chartXAxisLabel("Names") // 设置横轴标题
.frame(height: 300)
.padding()
Chart {
	LineMark(
		x: .value("Name", "Alice"),
		y: .value("Height (cm)", 165)
	)
	LineMark(
		x: .value("Name", "Bob"),
		y: .value("Height (cm)", 180)
	)
	LineMark(
		x: .value("Name", "Charlie"),
		y: .value("Height (cm)", 172)
	)
}
.chartYAxisLabel("Height (cm)") // 设置纵轴标题
.chartXAxisLabel("Names") // 设置横轴标题
.frame(height: 300)
.padding()

代码解释

  • LineMark:替代了之前的 BarMark,将数据展示为折线图。折线图更适合展示连续变化的趋势(虽然这里是离散数据,但仍然可以展示)。
  • 效果:你会看到一个折线图,连接了 Alice、Bob 和 Charlie 的身高数据点,而不是独立的柱子。

步骤 2:自定义图表样式

我们可以为图表添加颜色、符号等样式,以增强视觉效果。

示例代码:自定义折线图样式

swift
Chart {
    LineMark(
        x: .value("Name", "Alice"),
        y: .value("Height (cm)", 165)
    )
    .foregroundStyle(.red) // 设置折线颜色,第一个生效
    .symbol(.circle) // 在数据点上显示圆形符号

    LineMark(
        x: .value("Name", "Bob"),
        y: .value("Height (cm)", 180)
    )
    .foregroundStyle(.purple) // not work
    .symbol(.cross)

    LineMark(
        x: .value("Name", "Charlie"),
        y: .value("Height1 (cm)", 172)
    )
    .symbol(.asterisk)
}
.chartYAxisLabel("Height (cm)")
.chartXAxisLabel("Names")
.chartYScale(domain: 150 ... 200) // 设置纵轴范围
.chartXAxis {
    AxisMarks(values: .automatic) { _ in
        AxisGridLine() // 显示 X 轴网格线 (竖线
        AxisValueLabel() // 显示 X 轴刻度标签
    }
}
.chartYAxis {
    AxisMarks(values:.automatic)
    { _ in
        AxisGridLine() // 显示纵轴网格线(横线
        AxisValueLabel() // 显示纵轴标签
    }
}
Chart {
    LineMark(
        x: .value("Name", "Alice"),
        y: .value("Height (cm)", 165)
    )
    .foregroundStyle(.red) // 设置折线颜色,第一个生效
    .symbol(.circle) // 在数据点上显示圆形符号

    LineMark(
        x: .value("Name", "Bob"),
        y: .value("Height (cm)", 180)
    )
    .foregroundStyle(.purple) // not work
    .symbol(.cross)

    LineMark(
        x: .value("Name", "Charlie"),
        y: .value("Height1 (cm)", 172)
    )
    .symbol(.asterisk)
}
.chartYAxisLabel("Height (cm)")
.chartXAxisLabel("Names")
.chartYScale(domain: 150 ... 200) // 设置纵轴范围
.chartXAxis {
    AxisMarks(values: .automatic) { _ in
        AxisGridLine() // 显示 X 轴网格线 (竖线
        AxisValueLabel() // 显示 X 轴刻度标签
    }
}
.chartYAxis {
    AxisMarks(values:.automatic)
    { _ in
        AxisGridLine() // 显示纵轴网格线(横线
        AxisValueLabel() // 显示纵轴标签
    }
}

代码解释

  • .foregroundStyle(.blue):设置折线颜色为蓝色。(设置第一个元素生效)
  • .symbol(.circle):在每个数据点上显示一个圆形符号,增强可视化效果。
  • .chartYScale(domain: 150...200):限制纵轴的范围在 150 到 200 之间,让图表更聚焦。
  • .chartXAxis 和 .chartYAxis:自定义横轴和纵轴的网格线和标签,增加图表的可读性。

关于 chartYAxis & chartXAxis 概述

  • chartXAxis:用于自定义图表的横轴(X轴),即通常位于图表底部的轴。它可以控制 X 轴的刻度(Ticks)、标签(Labels)、网格线(Grid Lines)等。
  • chartYAxis:用于自定义图表的纵轴(Y轴),即通常位于图表左侧或右侧的轴。它可以控制 Y 轴的刻度、标签、网格线等。

这两个修饰符接受一个 View 作为参数,通常使用 AxisMarks 来定义轴上的标记和样式。AxisMarks 是一个构建器(Builder),允许你指定刻度的位置、样式以及相关的标签和网格线。

基本用法

在 SwiftUI Charts 中,chartXAxischartYAxis 通常与 AxisMarks 结合使用。AxisMarks 提供了一种声明式的方式来定义轴上的刻度标记。

1). values 参数

values 用于指定刻度的位置,可以是以下几种类型:

  • .automatic:由 SwiftUI 自动决定刻度的位置和数量,基于数据范围和图表大小。这是最常用的选项,适合大多数场景。
  • .stride(by:):按照固定步长生成刻度,适用于数值轴(如 Y 轴)。例如,values: .stride(by: 10) 会在 Y 轴上每隔 10 个单位生成一个刻度。
  • 数组 [Value]:手动指定刻度的位置。例如,values: [160, 170, 180] 会在 Y 轴的 160、170、180 处生成刻度。

2). 闭包参数

AxisMarks 的闭包参数允许你为每个刻度自定义样式。闭包接收一个 value 参数,表示当前刻度的值(类型取决于轴的数据类型,例如 Y 轴可能是 Double,X 轴可能是 String)。

在闭包中,你可以返回以下视图来定义刻度的样式:

  • AxisGridLine():绘制网格线。可以自定义线条样式(如颜色、宽度)。
  • AxisValueLabel():显示刻度标签。可以自定义文本样式或内容。
  • AxisTick():绘制刻度线(短线标记)。可以自定义长度和样式。

示例:基础的 X 轴和 Y 轴自定义

以下是一个简单的示例,展示如何为 X 轴和 Y 轴添加刻度标签和网格线 (默认情况都是显示的):

swift
.chartXAxis {
	AxisMarks(values: .automatic) { _ in
		AxisGridLine() // 显示 X 轴网格线 ( 竖线
		AxisValueLabel() // 显示 X 轴刻度标签
	}
}
.chartYAxis {
	AxisMarks(values: .automatic) { _ in
		AxisGridLine() // 显示 Y 轴网格线 (横线
		AxisValueLabel() // 显示 Y 轴刻度标签
	}
}
.chartXAxis {
	AxisMarks(values: .automatic) { _ in
		AxisGridLine() // 显示 X 轴网格线 ( 竖线
		AxisValueLabel() // 显示 X 轴刻度标签
	}
}
.chartYAxis {
	AxisMarks(values: .automatic) { _ in
		AxisGridLine() // 显示 Y 轴网格线 (横线
		AxisValueLabel() // 显示 Y 轴刻度标签
	}
}

手动指定 X 轴刻度:
这段代码只在 "Alice" 和 "Bob" 处显示刻度和标签,忽略其他类别。

swift
.chartXAxis {             
	// 这段代码只在 "Alice" 和 "Bob" 处显示刻度和标签,忽略其他类别。
	AxisMarks(values: ["Alice", "Bob"]) { value in
	   AxisGridLine()
	   AxisValueLabel()
	}
}
.chartYAxis {
	AxisMarks(values:.automatic)
	{ _ in
		AxisGridLine() // 显示纵轴网格线(横线
		AxisValueLabel() // 显示纵轴标签
	}
}
.chartXAxis {             
	// 这段代码只在 "Alice" 和 "Bob" 处显示刻度和标签,忽略其他类别。
	AxisMarks(values: ["Alice", "Bob"]) { value in
	   AxisGridLine()
	   AxisValueLabel()
	}
}
.chartYAxis {
	AxisMarks(values:.automatic)
	{ _ in
		AxisGridLine() // 显示纵轴网格线(横线
		AxisValueLabel() // 显示纵轴标签
	}
}

Y 轴每隔 5 个单位显示一个刻度和标签:

swift
.chartYAxis {
    AxisMarks(values:
        // Y 轴每隔 5 个单位显示一个刻度和标签
        .stride(by: 5))
    { _ in
        AxisGridLine() // 显示纵轴网格线(横线
        AxisValueLabel() // 显示纵轴标签
    }
}
.chartYAxis {
    AxisMarks(values:
        // Y 轴每隔 5 个单位显示一个刻度和标签
        .stride(by: 5))
    { _ in
        AxisGridLine() // 显示纵轴网格线(横线
        AxisValueLabel() // 显示纵轴标签
    }
}

定制化网格线的颜色
swift
.chartYAxis {
    AxisMarks(values: .stride(by: 5)) { _ in
        AxisGridLine()
            .foregroundStyle(.red.opacity(0.3)) // 设置网格线颜色和透明度
        AxisValueLabel()
            .foregroundStyle(.green) // 设置标签颜色
        AxisTick()
            .foregroundStyle(.yellow) // 设置刻度线颜色
    }
}
.chartYAxis {
    AxisMarks(values: .stride(by: 5)) { _ in
        AxisGridLine()
            .foregroundStyle(.red.opacity(0.3)) // 设置网格线颜色和透明度
        AxisValueLabel()
            .foregroundStyle(.green) // 设置标签颜色
        AxisTick()
            .foregroundStyle(.yellow) // 设置刻度线颜色
    }
}

刻度为指定值的时候进行高亮
swift
.chartYAxis {
    AxisMarks(values: .stride(by: 5)) { value in
        AxisGridLine()
        if value.as(Double.self) == 170 {
            AxisValueLabel()
                .foregroundStyle(.red) // 特定值高亮
        } else {
            AxisValueLabel()
        }
        AxisTick()
    }
}
.chartYAxis {
    AxisMarks(values: .stride(by: 5)) { value in
        AxisGridLine()
        if value.as(Double.self) == 170 {
            AxisValueLabel()
                .foregroundStyle(.red) // 特定值高亮
        } else {
            AxisValueLabel()
        }
        AxisTick()
    }
}

定制Y label单位
swift
.chartYAxis {
	AxisMarks(values: .stride(by: 10)) { value in
		AxisGridLine()
		AxisValueLabel("\(value.as(Double.self) ?? 0) cm") // 添加单位
		AxisTick()
	}
}
.chartYAxis {
	AxisMarks(values: .stride(by: 10)) { value in
		AxisGridLine()
		AxisValueLabel("\(value.as(Double.self) ?? 0) cm") // 添加单位
		AxisTick()
	}
}

点击交互示例

swift
import Charts
import SwiftUI

struct Person: Identifiable {
    let id = UUID()
    let name: String
    let height: Double
}

struct AnotherChart: View {
    let people = [
        Person(name: "Alice", height: 165),
        Person(name: "Bob", height: 180),
        Person(name: "Charlie", height: 172)
    ]

    @State private var selectedPerson: Person?

    var body: some View {
        VStack {
            if let person = selectedPerson {
                Text("Selected: \(person.name), Height: \(person.height) cm")
                    .font(.headline)
                    .padding()
            } else {
                Text("Tap a data point to see details")
                    .font(.headline)
                    .padding()
            }
            Chart(people) { person in
                LineMark(
                    x: .value("Name", person.name),
                    y: .value("Height (cm)", person.height)
                )
                .foregroundStyle(.blue)
                .symbol(.circle)
            }
            .chartYAxisLabel("Height (cm)")
            .chartXAxisLabel("Names")
            .chartYScale(domain: 150 ... 200)
            .chartOverlay { proxy in
                GeometryReader { _ in
                    Rectangle().fill(.clear).contentShape(Rectangle())
                        .gesture(
                            // 专为 Charts 等需要位置的场景设计
                            // 可以获取点击的图标
                            SpatialTapGesture()
                                .onEnded { value in
                                    let location = value.location
                                    if let (x, y) = proxy.value(at: location) as (String, Double)? {
                                        if let person = people.first(where: { $0.name == x }) {
                                            selectedPerson = person
                                        }
                                    }
                                }
                        )
                }
            }
            .frame(height: 300)
            .padding()
        }
    }
}
import Charts
import SwiftUI

struct Person: Identifiable {
    let id = UUID()
    let name: String
    let height: Double
}

struct AnotherChart: View {
    let people = [
        Person(name: "Alice", height: 165),
        Person(name: "Bob", height: 180),
        Person(name: "Charlie", height: 172)
    ]

    @State private var selectedPerson: Person?

    var body: some View {
        VStack {
            if let person = selectedPerson {
                Text("Selected: \(person.name), Height: \(person.height) cm")
                    .font(.headline)
                    .padding()
            } else {
                Text("Tap a data point to see details")
                    .font(.headline)
                    .padding()
            }
            Chart(people) { person in
                LineMark(
                    x: .value("Name", person.name),
                    y: .value("Height (cm)", person.height)
                )
                .foregroundStyle(.blue)
                .symbol(.circle)
            }
            .chartYAxisLabel("Height (cm)")
            .chartXAxisLabel("Names")
            .chartYScale(domain: 150 ... 200)
            .chartOverlay { proxy in
                GeometryReader { _ in
                    Rectangle().fill(.clear).contentShape(Rectangle())
                        .gesture(
                            // 专为 Charts 等需要位置的场景设计
                            // 可以获取点击的图标
                            SpatialTapGesture()
                                .onEnded { value in
                                    let location = value.location
                                    if let (x, y) = proxy.value(at: location) as (String, Double)? {
                                        if let person = people.first(where: { $0.name == x }) {
                                            selectedPerson = person
                                        }
                                    }
                                }
                        )
                }
            }
            .frame(height: 300)
            .padding()
        }
    }
}