Как добавить линию тренда на линейный график? (Две линейные отметки на одном графике)

Как я могу создать эту желаемую диаграмму с помощью диаграмм SwiftUI от Apple?

Желаемая диаграмма (макет). Ступенчатый линейный график с точками данных выделен синим цветом, а линия тренда — красным:

Линию тренда необходимо добавить к существующей диаграмме, содержащей ступенчатый линейный график. Никакие отметки SwiftUI Chart, похоже, не применимы к наклонной линии тренда, кроме использования другого LineMark.

Код линии тренда, представленный ниже, работает только при использовании с диаграммой рассеяния (PointMark). Но когда линия тренда используется с линейным графиком, то есть с другим LineMark, тогда линия тренда становится продолжением первого LineMark. Когда используются два LineMark, модификаторы второго LineMark игнорируются.

Нежелательный результат. При использовании двух линейных отметок в диаграммах SwiftUI это нежелательный результат вместо диаграммы выше:

Тем не менее, код линии тренда работает так, как хотелось бы, когда другой LineMark удален:

Полный пример кода с примерами данных:

import SwiftUI
import Charts

struct ContentView: View {
    
    @State var showSteps: Bool = false
    @State var showTrend: Bool = false
    
    var body: some View {
        VStack {
            Chart {
                
                    //Main data
                ForEach(dataPoints.filter({ data in data.type == .data}), id: \.id) { point in
                    if showSteps {
                        LineMark(
                            x: PlottableValue.value("Date", point.date),
                            y: PlottableValue.value("Amount", point.amount)
                        )
                        .interpolationMethod(.stepEnd)
                    }
                    
                    PointMark(
                        x: .value("Date", point.date),
                        y: .value("Amount", point.amount)
                    )
                }
                
                    // Trend line
                if showTrend {
                    ForEach(dataPoints.filter({ data in data.type == .trend}), id: \.id) { trend in
                        LineMark(
                            x: PlottableValue.value("Date", trend.date),
                            y: PlottableValue.value("Amount", trend.amount)
                        )
                        .foregroundStyle(Color.red)
                        .lineStyle(StrokeStyle(lineWidth: 5))
                        .interpolationMethod(.linear)
                    }
                }
            }
            .padding(.all)
            .frame(height: 500)
            .chartXScale(domain: Date(timeIntervalSinceNow: 3600 * 24 * -15) ... Date(timeIntervalSinceNow: 3600 * 24 * 15))
            .chartYAxis {
                AxisMarks(preset: .aligned, position: .automatic, values: .stride(by: 5)) {
                    let value = $0.as(Int.self) ?? 0
                    AxisGridLine()
                    AxisValueLabel(String("\(value)"))
                }
            }
            //.chartScrollableAxes(.horizontal) // future use
            Group {
                Toggle("Show Steps", isOn: $showSteps)
                Toggle("Show Trend", isOn: $showTrend)
            }.padding(.horizontal)
        }
    }
}

enum DataType {
    case data
    case trend
}

struct DataPoint: Hashable, Identifiable {
    var id       : UUID = UUID()
    var type   : DataType
    var date   : Date
    var amount : Double
}

let dataPoints: [DataPoint] = [
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -14), amount: 100),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -13), amount: 97),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -10), amount: 85),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -9), amount: 84),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -8), amount: 82),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -7), amount: 78),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -6), amount: 75),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -5), amount: 68),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -4), amount: 65),
    DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -3), amount: 50),
    DataPoint(id: UUID(), type: .data, date: Date(), amount: 48),
    DataPoint(id: UUID(), type: .trend, date: Date(timeIntervalSinceNow: 3600 * 24 * -15), amount: 100),
    DataPoint(id: UUID(), type: .trend, date: Date(timeIntervalSinceNow: 3600 * 24 * 15), amount: 0)
]

Решение должно будет работать с прокручиваемым графиком, поскольку окончательный график будет использовать большой таймфрейм. (модификатор прокрутки включен, но закомментирован)

Пробовал различные методы интерполяции на линейных графиках, разделяя данные на два массива, используя наложение диаграммы и создавая линию тренда с помощью CGPath. Один из них может быть решением, но я, возможно, упустил, как его правильно настроить.

Есть какие-нибудь советы о том, как заставить желаемую линию тренда правильно работать с другим LineMark? Или как создать линию тренда без использования LineMark?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
108
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Отметки линии тренда и фактических данных должны находиться в разных сериях. В противном случае предполагается, что они находятся в одной серии, и точки будут соединены вместе.

Добавьте параметр series: и задайте им разные PlottableValue.

if showSteps {
    LineMark(
        x: .value("Date", point.date),
        y: .value("Amount", point.amount),
        series: .value("Series", "Actual Data")
    )
    .interpolationMethod(.stepEnd)
}
if showTrend {
    ForEach(dataPoints.filter({ data in data.type == .trend}), id: \.id) { trend in
        LineMark(
            x: .value("Date", trend.date),
            y: .value("Amount", trend.amount),
            series: .value("Series", "Trend")
        )
        .foregroundStyle(Color.red)
        .lineStyle(StrokeStyle(lineWidth: 5))
        .interpolationMethod(.linear)
    }
}

ОЙ!! Оно работает. Не осознавал, что сериал так работает и нужен. Я думал, что фильтра будет достаточно. Я бы никогда не добрался туда без вашей помощи. Спасибо.

Marcy 18.04.2024 16:23

Другие вопросы по теме