当我尝试使用
onTapGesture
显示注释时,它出现在图表的中间。
当我单击该列时,它应该在该列的顶部显示我的注释,如下面的屏幕截图所示。我已经尝试了一切,但它总是显示在图表的中间。我该怎么做?
import SwiftUI
import Charts
struct FocusChartView: View {
var colorStyleTask: Array<Color> = [Color(.black).opacity(0.7), Color(.white)]
let focusStats: [FocusTime] = [
.init(date: Date.from(year: 2024, month: 1, day: 1), focusCount: 1),
.init(date: Date.from(year: 2024, month: 2, day: 1), focusCount: 3),
.init(date: Date.from(year: 2024, month: 3, day: 1), focusCount: 7),
.init(date: Date.from(year: 2024, month: 4, day: 1), focusCount: 12),
.init(date: Date.from(year: 2024, month: 5, day: 1), focusCount: 5),
.init(date: Date.from(year: 2024, month: 6, day: 1), focusCount: 12),
.init(date: Date.from(year: 2024, month: 7, day: 1), focusCount: 1),]
var body: some View {
VStack {
taskCharts
}
}
private var taskCharts: some View {
ZStack {
Color(.gray)
.ignoresSafeArea()
Chart {
let maxViews = focusStats.map { $0.focusCount }.max() ?? 1
ForEach(focusStats) { viewMonth in
let viewCount = Double(viewMonth.focusCount)
let scaledHeight = (viewCount / Double(maxViews)) * 90
let relativeHeight = max(42.0, scaledHeight)
BarMark(
x: .value("Month", viewMonth.date, unit: .month),
y: .value("Views", relativeHeight)
)
.foregroundStyle(
SwiftUI.LinearGradient(
gradient: SwiftUI.Gradient(colors: colorStyleTask),
startPoint: .bottom,
endPoint: .top
)
)
.clipShape(RoundedRectangle(cornerRadius: 5))
.annotation(position: .top) {
VStack {
Text(viewMonth.date.formattedToMonthAndYear())
Text(viewMonth.focusCount.toHoursAndMinutes())
.fontWeight(.bold)
}
.padding(.top, 8)
.padding(.horizontal, 12)
.background(Color(.white))
.cornerRadius(5)
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 3)
.offset(y: -3)
}
}
}
.frame(height: 151)
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
}
}
.chartYAxis {
}
}
}
}
#Preview {
FocusChartView()
}
struct FocusTime: Identifiable {
let id = UUID()
let date: Date
let focusCount: Int
}
extension Int {
func toHoursAndMinutes() -> String {
let hours = self / 60
let minutes = self % 60
return "\(hours)h \(minutes)m"
}
}
extension Date {
static func from(year: Int, month: Int, day: Int) -> Date {
let components = DateComponents(year: year, month: month, day: day)
return Calendar.current.date(from: components)!
}
func formattedToMonthAndYear() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM, yyyy"
return formatter.string(from: self)
}
}
要在点击/单击图表栏时在图表栏顶部显示注释, 使用
var tapedDate
和 .chartGesture
尝试此方法
如示例代码所示。
struct ContentView: View {
var body: some View {
FocusChartView()
}
}
struct FocusChartView: View {
var colorStyleTask = [Color(.black).opacity(0.7), Color(.white)]
let focusStats: [FocusTime] = [
.init(date: Date.from(year: 2024, month: 1, day: 1), focusCount: 1),
.init(date: Date.from(year: 2024, month: 2, day: 1), focusCount: 3),
.init(date: Date.from(year: 2024, month: 3, day: 1), focusCount: 7),
.init(date: Date.from(year: 2024, month: 4, day: 1), focusCount: 12),
.init(date: Date.from(year: 2024, month: 5, day: 1), focusCount: 5),
.init(date: Date.from(year: 2024, month: 6, day: 1), focusCount: 12),
.init(date: Date.from(year: 2024, month: 7, day: 1), focusCount: 1)]
@State private var tapedDate: Date? // <-- here
var body: some View {
VStack {
taskCharts
}
}
@ViewBuilder
private var taskCharts: some View {
ZStack {
Color(.gray)
.ignoresSafeArea()
Chart {
let maxViews = focusStats.map { $0.focusCount }.max() ?? 1
ForEach(focusStats) { viewMonth in
let viewCount = Double(viewMonth.focusCount)
let scaledHeight = (viewCount / Double(maxViews)) * 90
let relativeHeight = max(42.0, scaledHeight)
BarMark(
x: .value("Month", viewMonth.date, unit: .month),
y: .value("Views", relativeHeight)
)
.foregroundStyle(
SwiftUI.LinearGradient(
gradient: SwiftUI.Gradient(colors: colorStyleTask),
startPoint: .bottom,
endPoint: .top
)
)
.clipShape(RoundedRectangle(cornerRadius: 5))
.annotation(position: .top) {
if sameMonth(date: tapedDate, tgt: viewMonth.date) { // <--- here
VStack {
Text(viewMonth.date.formattedToMonthAndYear())
Text(viewMonth.focusCount.toHoursAndMinutes())
.fontWeight(.bold)
}
.padding(.top, 8)
.padding(.horizontal, 12)
.background(Color(.white))
.cornerRadius(5)
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 3)
.offset(y: -3)
} else {
Text("").frame(height: 55) // <--- here, or adjust the Yscale
}
}
}
}
.frame(height: 151)
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
}
}
.chartGesture { proxy in // <-- here
SpatialTapGesture()
.onEnded { item in
if let target: (date: Date, temp: Double) = proxy.value(at: item.location, as: (Date, Double).self),
let chartData = closestChartData(to: target) {
tapedDate = chartData.date
}
}
}
}
}
func closestChartData(to target: (date: Date, temp: Double)) -> FocusTime? {
var closestChartData: FocusTime?
for datum in focusStats {
if sameMonth(date: datum.date, tgt: target.date) {
closestChartData = datum
break
}
}
return closestChartData
}
func sameMonth(date: Date?, tgt: Date) -> Bool {
if let theDate = date {
let month = Calendar.current.dateComponents([.month], from: theDate, to: tgt)
return month.month == 0 ? true : false
}
return false
}
}