I have implemented a drag line into this chart, however the drag line does not start has the beginning of the charts plotted bars. The drag line is also not lining up with cursor. I have been attempting fix the implementation but I can't get the cursor and drag line to line up nor the drag line to start at the beginning chat.
struct EnergyUsageBarChartView: View {
@Environment(\.colorScheme) var scheme
@StateObject var viewModel: UtilityEventsViewModel
@State private var selectedDays = 10
@State private var dragPosition = CGPoint.zero
@State private var dragValue: Double = 0.0
@State private var isDragging = false
@State var plotWidth: CGFloat = 0
var body: some View {
NavigationStack {
VStack {
VStack(alignment: .leading, spacing: 7) {
HStack {
Text("Views")
.fontWeight(.semibold)
Picker("Select Range", selection: $selectedDays) {
Text("Past Day").tag(1)
Text("Past 10 Days").tag(10)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
.onChange(of: selectedDays) { _ in
viewModel.fetchUtilityEvents(forLastDays: selectedDays)
}
}
HStack {
Text("\(viewModel.totalEnergyUsed, specifier: "%.2f") kWh")
.font(.largeTitle.bold())
}
UtilityChart()
}
.padding()
.background {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill((scheme == .dark ? Color.black : Color.white).shadow(.drop(radius: 2)))
}
.onAppear {
viewModel.fetchUtilityEvents(forLastDays: selectedDays)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding()
.navigationTitle("Energy Usage")
}
}
@ViewBuilder
func UtilityChart() -> some View {
let dailyTotals = aggregateEnergyByDay().sorted(by: { $0.key < $1.key })
if dailyTotals.isEmpty {
Text("No data available")
.foregroundColor(.gray)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
} else {
let averageEnergy = viewModel.averageEnergyUsed
ZStack {
Chart {
ForEach(Array(dailyTotals.enumerated()), id: \.element.key) { index, element in
// "In" value bar
if element.value.inTotal > 0 {
BarMark(
x: .value("Day", Calendar.current.startOfDay(for: element.key)),
y: .value("In Energy (kWh)", element.value.inTotal)
)
.foregroundStyle(Color.green)
}
// "Out" value bar, plotted negatively to distinguish from "In"
if element.value.outTotal > 0 {
BarMark(
x: .value("Day", Calendar.current.startOfDay(for: element.key).addingTimeInterval(86400 / 2)), // Offset by half a day
y: .value("Out Energy (kWh)", -element.value.outTotal)
)
.foregroundStyle(Color.blue)
}
}
/*
RuleMark(y: .value("Average Energy Usage", averageEnergy))
.lineStyle(StrokeStyle(lineWidth: 2, dash: [5]))
.foregroundStyle(.red)
.annotation(position: .top, alignment: .trailing) {
Text("Avg: \(averageEnergy, specifier: "%.2f") kWh")
.font(.caption)
.foregroundColor(.red)
.padding(4)
.background(.white.opacity(0.5))
.cornerRadius(5)
}
*/
}
.frame(height: 350)
.overlay(
GeometryReader { geometry in
if isDragging {
let (inValue, outValue) = calculateValueAtDragPosition(geometry: geometry)
VStack {
if inValue > 0 {
Text("In: \(inValue, specifier: "%.2f") kWh")
.padding(5)
.background(Color.green.opacity(0.75))
.foregroundColor(Color.black)
.cornerRadius(5)
.position(x: 150, y: 385)
}
if outValue > 0 {
Text("Out: \(outValue, specifier: "%.2f") kWh")
.padding(5)
.background(Color.green.opacity(0.75))
.foregroundColor(Color.black)
.cornerRadius(5)
.position(x: 150, y: 385)
}
Rectangle()
.fill(Color.blue)
.frame(width: 2, height: geometry.size.height)
.offset(x: dragPosition.x - geometry.frame(in: .local).minX)
}
}
}
)
.gesture(
DragGesture()
.onChanged { value in
dragPosition = CGPoint(x: value.location.x, y: value.location.y)
isDragging = true
}
.onEnded { _ in
isDragging = false
}
)
}
LegendView()
}
}
func aggregateEnergyByDay() -> [Date: (inTotal: Double, outTotal: Double)] {
var dailyTotals: [Date: (inTotal: Double, outTotal: Double)] = [:]
let calendar = Calendar.current
for event in viewModel.utilityEvents {
guard case let .intervalReading(reading) = event.value else { continue }
let date = calendar.startOfDay(for: reading.start)
if reading.flowDirection == "Out" {
let currentOutTotal = dailyTotals[date]?.outTotal ?? 0
dailyTotals[date] = (dailyTotals[date]?.inTotal ?? 0, currentOutTotal + reading.value)
} else {
let currentInTotal = dailyTotals[date]?.inTotal ?? 0
dailyTotals[date] = (currentInTotal + reading.value, dailyTotals[date]?.outTotal ?? 0)
}
}
return dailyTotals
}
func calculateValueAtDragPosition(geometry: GeometryProxy) -> (inValue: Double, outValue: Double) {
let chartWidth = geometry.size.width // Consider chart's actual data width if padding/margin is known
let adjustedX = max(min(dragPosition.x, chartWidth), 0) // Adjust drag within chart bounds
let positionRatio = adjustedX / chartWidth
let dailyTotalsArray = aggregateEnergyByDay().sorted(by: { $0.key < $1.key })
let totalBars = dailyTotalsArray.count
let selectedBarIndex = Int(floor(positionRatio * Double(totalBars)))
let selectedDayValues = dailyTotalsArray[max(0, min(selectedBarIndex, dailyTotalsArray.count - 1))].value
let segmentWidth = 1.0 / Double(totalBars)
let isInValue = (positionRatio - Double(selectedBarIndex) * segmentWidth) < (segmentWidth / 2)
return isInValue ? (selectedDayValues.inTotal, 0) : (0, selectedDayValues.outTotal)
}
}