Когда речь идет о представлении информации пользователям, проще — значит лучше.
Особенно когда вы имеете дело с большими наборами данных. У вас есть различные варианты, включая пользовательские представления, таблицы, сводки. Однако можно использовать более впечатляющую форму подачи визуального контента — графическое представление.
Графики позволяют пользователям с первого взгляда точно оценить представленные данные.
Это краткое и легко выполнимое руководство по использованию графиков поможет повысить вовлеченность пользователей приложения.
Базовые понятия
График создается путем выстраивания серии элементов. Эти элементы должны соответствовать протоколу ChartContent
и представлять типы, которые могут быть выведены в области видимости графика.
Для создания графика используется метод init(content:)
. В замыкании ViewBuilder добавляются все необходимые визуальные элементы.
struct ChartView: View {
var body: some View {
ChartView {
// Элементы графика
}
}
}
Какие элементы можно использовать?
Во фреймворке Charts
есть предустановленный набор готовых к использованию элементов ChartContent
, называемых Marks (метками). Каждую метку можно рассматривать как графический элемент, представляющий данные.
У каждого типа меток есть несколько инициализаторов, выбор которых зависит от того, какой пользовательский интерфейс вы хотите создать.
Можно использовать 3 типа данных в графиках.
- Количественные: числовые значения, такие как Int (целочисленное), Double (с двойной точностью) и Float (с плавающей точкой).
- Номинальные: дискретные категории, или группы.
- Временные: точки во времени.
В зависимости от используемого типа данных, применяются различные конфигурации для управления пользовательским интерфейсом графика.
Перейдем к коду
В целях демонстрации будем работать с визуальным представлением количества кофе, потребляемого пользователями с течением времени и доступного в 4 видах: Latte (латте), Cappuccino (капучино), Cortado (кортадо) и FlatWhite (кофе с молоком),
Для начала создадим простую гистограмму, отражающую общее количество кофе.
struct CoffeeData: Identifiable {
typealias CoffeeDetails = (type: Coffee, amount: Int)
let id = UUID()
let date: Date
let details: [CoffeeDetails]
static func mockData() -> [CoffeeData] { ... }
}
struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()
var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
BarMark(
x: .value("Date", coffeeInfo.date),
y: .value("Coffee", totalCoffees(in: coffeeInfo.details))
)
}
}
.frame(height: 300)
.padding()
}
func totalCoffees(in details: [CoffeeData.CoffeeDetails]) -> Int {
return details.map({$0.amount}).reduce(0, +)
}
}
Кастомизация графика
Чтобы разграничить данные по видам кофе, нужно выполнить дополнительную итерацию через CoffeeDetails
и использовать модификатор foregroundStyle(by:)
для группирования данных.
struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()
var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
ForEach(coffeeInfo.details, id: \.type) { coffeeDetails in
BarMark(
x: .value("Date", coffeeInfo.date),
y: .value("Coffee", coffeeDetails.amount)
)
.foregroundStyle(by: .value("Coffee Type", coffeeDetails.type))
}
}
}
.frame(height: 300)
.padding()
}
}
Небольшая корректировка позволила получить сгруппированные данные. Однако такой тип графика в основном используется при необходимости отразить прогресс по определенным значениям.
В данном случае надо получить по одному столбику для каждого вида кофе, а это значит, что для каждого значения X (оно же месяц) понадобится 4 столбика (Latte/Cappuccino/Cortado/FlatWhite). Для выполнения этой задачи потребуется внести два изменения.
- Использовать опцию
unit
по отношению к значениям оси X, чтобы указать, что надо сгруппировать значения по месяцам. - Использовать модификатор
position(by:axis:span:)
, чтобы создать метку для гистограммы.
struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()
var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
ForEach(coffeeInfo.details, id: \.type) { coffeeDetails in
BarMark(
x: .value("Date", coffeeInfo.date, unit: .month),
y: .value("Coffee", coffeeDetails.amount)
)
.foregroundStyle(by: .value("Coffee Type", coffeeDetails.type))
.position(by: .value("Coffee Type", coffeeDetails.type))
}
}
}
.frame(height: 300)
.padding()
}
}
Возможности модификации графиков
Продолжим модифицировать приведенный выше график в соответствии с пользовательскими потребностями.
Пользовательские цвета столбиков
Используйте модификатор chartForegroundStyleScale(_:)
. Нужно просто присвоить значение каждому группируемому элементу. В данном случае: Latte, Cappuccino, Cortado и FlatWhite.
Изменение масштаба
Если хотите управлять значениями, отображаемыми на оси, чтобы увеличить или уменьшить метки графика, можете использовать модификаторы chartYScale(domain:type:)
и chartXScale(domain:type)
. Диапазон функциональной области может быть замкнутым (например, от 0 до 15) для данных количественного и временного типа или массивом значений для данных дискретного типа.
Настройка меток осей
В данном случае стоит отобразить на оси X месяц вместе с годом, например Aug (Август) 2023. Модификатор chartXAxis(content:)
позволяет это сделать.
Добавление аннотаций
Иногда для повышения читаемости следует включать в метки графика дополнительную информацию. Используя annotation(position:alignment:spacing:content)
, можно поместить любое представление вместе с метками.
struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()
var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
ForEach(coffeeInfo.details, id: \.type) { coffeeDetails in
BarMark(
x: .value("Date", coffeeInfo.date, unit: .month),
y: .value("Coffee", coffeeDetails.amount)
)
.annotation(position: .top, alignment: .center) {
Text("\(coffeeDetails.amount)")
}
.foregroundStyle(by: .value("Coffee Type", coffeeDetails.type))
.position(by: .value("Coffee Type", coffeeDetails.type))
.cornerRadius(12)
}
}
}
.chartForegroundStyleScale([
Coffee.latte: Color.accentColor,
Coffee.cappuccino: Color.accentColor.opacity(0.7),
Coffee.cortado: Color.accentColor.opacity(0.5),
Coffee.flatwhite: Color.accentColor.opacity(0.3),
])
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits), centered: true)
}
}
.chartScrollableAxes(.horizontal)
.chartYScale(domain: 0 ... 15)
.frame(height: 300)
.padding()
}
}
Комбинирование и интерактивность
Создавая графики, можно добавлять в них различные ChartComponent
. Эти компоненты не обязательно должны быть одного типа.
Чтобы добиться такого пользовательского интерфейса, нужно объединить LineMark
и AreaMark
.
struct OverallData: Identifiable {
let id = UUID()
let date: Date
let coffee: Int
static func mockData() -> [OverallData] {
return [
.init(date: Date(year: 2023, month: 08), coffee: 12),
.init(date: Date(year: 2023, month: 09), coffee: 15),
.init(date: Date(year: 2023, month: 10), coffee: 8),
.init(date: Date(year: 2023, month: 11), coffee: 18),
.init(date: Date(year: 2023, month: 12), coffee: 14),
.init(date: Date(year: 2024, month: 01), coffee: 22),
]
}
}
struct DemoChart: View {
@State private var overallData = OverallData.mockData()
private var areaBackground: Gradient {
return Gradient(colors: [Color.accentColor, Color.accentColor.opacity(0.1)])
}
var body: some View {
Chart(overallData) {
LineMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.symbol(.circle)
.interpolationMethod(.catmullRom)
AreaMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(areaBackground)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits), centered: true)
}
}
.chartYScale(domain: 0 ... 30)
.frame(height: 300)
.padding()
}
}
Можно к этой комбинации добавить RuleMark
вместе с пользовательским представлением в виде Annotation
, чтобы каждый пользователь мог выбрать определенную точку для просмотра значения.
struct DemoChart: View {
@Environment(\.calendar) var calendar
@State private var coffeeData = CoffeeData.mockData()
@State private var overallData = OverallData.mockData()
@State private var chartSelection: Date?
private var areaBackground: Gradient {
return Gradient(colors: [Color.accentColor, Color.accentColor.opacity(0.1)])
}
var body: some View {
Chart(overallData) {
LineMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.symbol(.circle)
.interpolationMethod(.catmullRom)
if let chartSelection {
RuleMark(x: .value("Month", chartSelection, unit: .month))
.foregroundStyle(.gray.opacity(0.5))
.annotation(
position: .top,
overflowResolution: .init(x: .fit, y: .disabled)
) {
ZStack {
Text("\(getCoffee(for: chartSelection)) coffees")
}
.padding()
.background {
RoundedRectangle(cornerRadius: 4)
.foregroundStyle(Color.accentColor.opacity(0.2))
}
}
}
AreaMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(areaBackground)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits), centered: true)
}
}
.chartYScale(domain: 0 ... 30)
.frame(height: 300)
.padding()
.chartXSelection(value: $chartSelection)
}
}
Выводы
- Прежде чем использовать графические элементы, подумайте, что вы хотите показать пользователям.
- Сосредоточьтесь на моделировании данных. От того, как смоделированы данные, напрямую зависит, насколько легко будет работать с графиком.
- Давайте пользователям первое представление о данных, которые они увидят на графике, предоставив информацию в сгруппированном виде.
- Помимо базовых навыков, есть множество других конфигураций и настроек для улучшения графиков. Рекомендую изучить официальную документацию Apple по Charts и посмотреть записи сессий WWDC (Worldwide Developers Conference — Всемирная конференция разработчиков, ежегодно проводимая Apple).
Читайте также:
- Как мобильному разработчику всегда быть в курсе последних событий в своей сфере
- Осваиваем ViewThatFits. Часть 2
- 5 актуальных расширений Xcode для оптимизации разработки
Читайте нас в Telegram, VK и Дзен
Перевод статьи Bruno Lorenzo: Introduction to Charts in SwiftUI