Swift drag line

24 views Asked by At

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)
}
}
0

There are 0 answers