USB-Meter / USB Meter / Views / Meter / Components / MeasurementChartView.swift
9010128 10 hours ago History
1 contributor
2780 lines | 100.374kb
//
//  MeasurementChartView.swift
//  USB Meter
//
//  Created by Bogdan Timofte on 06/05/2020.
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
//

import SwiftUI

private enum PresentTrackingMode: CaseIterable, Hashable {
    case keepDuration
    case keepStartTimestamp
}

struct MeasurementChartView: View {
    private enum SmoothingLevel: CaseIterable, Hashable {
        case off
        case light
        case medium
        case strong

        var label: String {
            switch self {
            case .off: return "Off"
            case .light: return "Light"
            case .medium: return "Medium"
            case .strong: return "Strong"
            }
        }

        var shortLabel: String {
            switch self {
            case .off: return "Off"
            case .light: return "Low"
            case .medium: return "Med"
            case .strong: return "High"
            }
        }

        var movingAverageWindowSize: Int {
            switch self {
            case .off: return 1
            case .light: return 5
            case .medium: return 11
            case .strong: return 21
            }
        }
    }

    private enum SeriesKind {
        case power
        case energy
        case voltage
        case current
        case temperature

        var unit: String {
            switch self {
            case .power: return "W"
            case .energy: return "Wh"
            case .voltage: return "V"
            case .current: return "A"
            case .temperature: return ""
            }
        }

        var tint: Color {
            switch self {
            case .power: return .red
            case .energy: return .teal
            case .voltage: return .green
            case .current: return .blue
            case .temperature: return .orange
            }
        }
    }

    private struct SeriesData {
        let kind: SeriesKind
        let points: [Measurements.Measurement.Point]
        let samplePoints: [Measurements.Measurement.Point]
        let context: ChartContext
        let autoLowerBound: Double
        let autoUpperBound: Double
        let maximumSampleValue: Double?
    }

    private let minimumTimeSpan: TimeInterval = 1
    private let minimumVoltageSpan = 0.5
    private let minimumCurrentSpan = 0.5
    private let minimumPowerSpan = 0.5
    private let minimumEnergySpan = 0.1
    private let minimumTemperatureSpan = 1.0
    private let defaultEmptyChartTimeSpan: TimeInterval = 60
    private let selectorTint: Color = .blue

    let compactLayout: Bool
    let availableSize: CGSize
    
    @EnvironmentObject private var measurements: Measurements
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    @Environment(\.verticalSizeClass) private var verticalSizeClass
    var timeRange: ClosedRange<Date>? = nil
    
    @State var displayVoltage: Bool = false
    @State var displayCurrent: Bool = false
    @State var displayPower: Bool = true
    @State var displayEnergy: Bool = false
    @State var displayTemperature: Bool = false
    @State private var smoothingLevel: SmoothingLevel = .off
    @State private var chartNow: Date = Date()
    @State private var selectedVisibleTimeRange: ClosedRange<Date>?
    @State private var isPinnedToPresent: Bool = false
    @State private var presentTrackingMode: PresentTrackingMode = .keepDuration
    @State private var pinOrigin: Bool = false
    @State private var useSharedOrigin: Bool = false
    @State private var sharedAxisOrigin: Double = 0
    @State private var sharedAxisUpperBound: Double = 1
    @State private var powerAxisOrigin: Double = 0
    @State private var energyAxisOrigin: Double = 0
    @State private var voltageAxisOrigin: Double = 0
    @State private var currentAxisOrigin: Double = 0
    @State private var temperatureAxisOrigin: Double = 0
    let xLabels: Int = 4
    let yLabels: Int = 4

    init(
        compactLayout: Bool = false,
        availableSize: CGSize = .zero,
        timeRange: ClosedRange<Date>? = nil
    ) {
        self.compactLayout = compactLayout
        self.availableSize = availableSize
        self.timeRange = timeRange
    }

    private var axisColumnWidth: CGFloat {
        if compactLayout {
            return 38
        }
        return isLargeDisplay ? 62 : 46
    }

    private var chartSectionSpacing: CGFloat {
        compactLayout ? 6 : 8
    }

    private var xAxisHeight: CGFloat {
        if compactLayout {
            return 24
        }
        return isLargeDisplay ? 36 : 28
    }

    private var isPortraitLayout: Bool {
        guard availableSize != .zero else { return verticalSizeClass != .compact }
        return availableSize.height >= availableSize.width
    }

    private var isIPhone: Bool {
        #if os(iOS)
        return UIDevice.current.userInterfaceIdiom == .phone
        #else
        return false
        #endif
    }

    private enum OriginControlsPlacement {
        case aboveXAxisLegend
        case overXAxisLegend
        case belowXAxisLegend
    }

    private var originControlsPlacement: OriginControlsPlacement {
        if isIPhone {
            return isPortraitLayout ? .aboveXAxisLegend : .overXAxisLegend
        }
        return .belowXAxisLegend
    }

    private var plotSectionHeight: CGFloat {
        if availableSize == .zero {
            return compactLayout ? 300 : 380
        }

        if isPortraitLayout {
            // Keep the rendered plot area (plot section minus X axis) above half of the display height.
            let minimumPlotHeight = max(availableSize.height * 0.52, compactLayout ? 250 : 320)
            return minimumPlotHeight + xAxisHeight
        }

        if compactLayout {
            return min(max(availableSize.height * 0.36, 240), 300)
        }

        return min(max(availableSize.height * 0.5, 300), 440)
    }

    private var stackedToolbarLayout: Bool {
        if availableSize.width > 0 {
            return availableSize.width < 640
        }

        return horizontalSizeClass == .compact && verticalSizeClass != .compact
    }

    private var showsLabeledOriginControls: Bool {
        !compactLayout && !stackedToolbarLayout
    }

    private var isLargeDisplay: Bool {
        #if os(iOS)
        if UIDevice.current.userInterfaceIdiom == .phone {
            return false
        }
        #endif

        if availableSize.width > 0 {
            return availableSize.width >= 900 || availableSize.height >= 700
        }
        return !compactLayout && horizontalSizeClass == .regular && verticalSizeClass == .regular
    }

    private var chartBaseFont: Font {
        if isIPhone && isPortraitLayout {
            return .caption
        }
        return isLargeDisplay ? .callout : .footnote
    }

    private var usesCompactLandscapeOriginControls: Bool {
        isIPhone && !isPortraitLayout && availableSize.width > 0 && availableSize.width <= 740
    }

    var body: some View {
        let availableTimeRange = availableSelectionTimeRange()
        let visibleTimeRange = resolvedVisibleTimeRange(within: availableTimeRange)
        let powerSeries = series(
            for: measurements.power,
            kind: .power,
            minimumYSpan: minimumPowerSpan,
            visibleTimeRange: visibleTimeRange
        )
        let energySeries = series(
            for: measurements.energy,
            kind: .energy,
            minimumYSpan: minimumEnergySpan,
            visibleTimeRange: visibleTimeRange
        )
        let voltageSeries = series(
            for: measurements.voltage,
            kind: .voltage,
            minimumYSpan: minimumVoltageSpan,
            visibleTimeRange: visibleTimeRange
        )
        let currentSeries = series(
            for: measurements.current,
            kind: .current,
            minimumYSpan: minimumCurrentSpan,
            visibleTimeRange: visibleTimeRange
        )
        let temperatureSeries = series(
            for: measurements.temperature,
            kind: .temperature,
            minimumYSpan: minimumTemperatureSpan,
            visibleTimeRange: visibleTimeRange
        )
        let primarySeries = displayedPrimarySeries(
            powerSeries: powerSeries,
            energySeries: energySeries,
            voltageSeries: voltageSeries,
            currentSeries: currentSeries
        )
        let selectorSeries = primarySeries.map { overviewSeries(for: $0.kind) }

        Group {
            if let primarySeries {
                VStack(alignment: .leading, spacing: 12) {
                    chartToggleBar()

                    GeometryReader { geometry in
                        let plotHeight = max(geometry.size.height - xAxisHeight, compactLayout ? 180 : 220)

                        VStack(spacing: 6) {
                            HStack(spacing: chartSectionSpacing) {
                                primaryAxisView(
                                    height: plotHeight,
                                    powerSeries: powerSeries,
                                    energySeries: energySeries,
                                    voltageSeries: voltageSeries,
                                    currentSeries: currentSeries
                                )
                                .frame(width: axisColumnWidth, height: plotHeight)

                                ZStack {
                                    RoundedRectangle(cornerRadius: 18, style: .continuous)
                                        .fill(Color.primary.opacity(0.05))

                                    RoundedRectangle(cornerRadius: 18, style: .continuous)
                                        .stroke(Color.secondary.opacity(0.16), lineWidth: 1)

                                    horizontalGuides(context: primarySeries.context)
                                    verticalGuides(context: primarySeries.context)
                                    discontinuityMarkers(points: primarySeries.points, context: primarySeries.context)
                                    renderedChart(
                                        powerSeries: powerSeries,
                                        energySeries: energySeries,
                                        voltageSeries: voltageSeries,
                                        currentSeries: currentSeries,
                                        temperatureSeries: temperatureSeries
                                    )
                                }
                                .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
                                .frame(maxWidth: .infinity)
                                .frame(height: plotHeight)

                                secondaryAxisView(
                                    height: plotHeight,
                                    powerSeries: powerSeries,
                                    energySeries: energySeries,
                                    voltageSeries: voltageSeries,
                                    currentSeries: currentSeries,
                                    temperatureSeries: temperatureSeries
                                )
                                .frame(width: axisColumnWidth, height: plotHeight)
                            }
                            .overlay(alignment: .bottom) {
                                if originControlsPlacement == .aboveXAxisLegend {
                                    scaleControlsPill(
                                        voltageSeries: voltageSeries,
                                        currentSeries: currentSeries
                                    )
                                    .padding(.bottom, compactLayout ? 6 : 10)
                                }
                            }

                            switch originControlsPlacement {
                            case .aboveXAxisLegend:
                                xAxisLabelsView(context: primarySeries.context)
                                    .frame(height: xAxisHeight)
                            case .overXAxisLegend:
                                xAxisLabelsView(context: primarySeries.context)
                                    .frame(height: xAxisHeight)
                                    .overlay(alignment: .center) {
                                        scaleControlsPill(
                                            voltageSeries: voltageSeries,
                                            currentSeries: currentSeries
                                        )
                                        .offset(y: usesCompactLandscapeOriginControls ? 2 : (compactLayout ? 8 : 10))
                                    }
                            case .belowXAxisLegend:
                                xAxisLabelsView(context: primarySeries.context)
                                    .frame(height: xAxisHeight)

                                HStack {
                                    Spacer(minLength: 0)
                                    scaleControlsPill(
                                        voltageSeries: voltageSeries,
                                        currentSeries: currentSeries
                                    )
                                    Spacer(minLength: 0)
                                }
                            }

                            if let availableTimeRange,
                               let selectorSeries,
                               shouldShowRangeSelector(
                                availableTimeRange: availableTimeRange,
                                series: selectorSeries
                               ) {
                                TimeRangeSelectorView(
                                    points: selectorSeries.points,
                                    context: selectorSeries.context,
                                    availableTimeRange: availableTimeRange,
                                    selectorTint: selectorTint,
                                    compactLayout: compactLayout,
                                    minimumSelectionSpan: minimumTimeSpan,
                                    onKeepSelection: trimBufferToSelection,
                                    onRemoveSelection: removeSelectionFromBuffer,
                                    onResetBuffer: resetBuffer,
                                    selectedTimeRange: $selectedVisibleTimeRange,
                                    isPinnedToPresent: $isPinnedToPresent,
                                    presentTrackingMode: $presentTrackingMode
                                )
                            }
                        }
                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
                    }
                    .frame(height: plotSectionHeight)
                }
            } else {
                VStack(alignment: .leading, spacing: 12) {
                    chartToggleBar()
                    Text("Select at least one measurement series.")
                        .foregroundColor(.secondary)
                }
            }
        }
        .font(chartBaseFont)
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { now in
            guard timeRange == nil else { return }
            chartNow = now
        }
    }

    private func chartToggleBar() -> some View {
        let condensedLayout = compactLayout || verticalSizeClass == .compact
        let sectionSpacing: CGFloat = condensedLayout ? 8 : (isLargeDisplay ? 12 : 10)

        let controlsPanel = HStack(alignment: .center, spacing: sectionSpacing) {
            seriesToggleRow(condensedLayout: condensedLayout)
        }
        .padding(.horizontal, condensedLayout ? 10 : (isLargeDisplay ? 14 : 12))
        .padding(.vertical, condensedLayout ? 8 : (isLargeDisplay ? 12 : 10))
        .background(
            RoundedRectangle(cornerRadius: condensedLayout ? 14 : 16, style: .continuous)
                .fill(Color.primary.opacity(0.045))
        )
        .overlay(
            RoundedRectangle(cornerRadius: condensedLayout ? 14 : 16, style: .continuous)
                .stroke(Color.secondary.opacity(0.14), lineWidth: 1)
        )

        return Group {
            if stackedToolbarLayout {
                controlsPanel
            } else {
                HStack(alignment: .top, spacing: isLargeDisplay ? 14 : 12) {
                    controlsPanel
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private var shouldFloatScaleControlsOverChart: Bool {
        #if os(iOS)
        if availableSize.width > 0, availableSize.height > 0 {
            return availableSize.width > availableSize.height
        }
        return horizontalSizeClass != .compact && verticalSizeClass == .compact
        #else
        return false
        #endif
    }

    private func scaleControlsPill(
        voltageSeries: SeriesData,
        currentSeries: SeriesData
    ) -> some View {
        let condensedLayout = compactLayout || verticalSizeClass == .compact
        let showsCardBackground = !(isIPhone && isPortraitLayout) && !shouldFloatScaleControlsOverChart
        let horizontalPadding: CGFloat = usesCompactLandscapeOriginControls ? 6 : (condensedLayout ? 10 : (isLargeDisplay ? 12 : 10))
        let verticalPadding: CGFloat = usesCompactLandscapeOriginControls ? 5 : (condensedLayout ? 8 : (isLargeDisplay ? 10 : 8))

        return originControlsRow(
            voltageSeries: voltageSeries,
            currentSeries: currentSeries,
            condensedLayout: condensedLayout,
            showsLabel: !shouldFloatScaleControlsOverChart && showsLabeledOriginControls
        )
        .padding(.horizontal, horizontalPadding)
        .padding(.vertical, verticalPadding)
        .background(
            Capsule(style: .continuous)
                .fill(showsCardBackground ? Color.primary.opacity(0.08) : Color.clear)
        )
        .overlay(
            Capsule(style: .continuous)
                .stroke(
                    showsCardBackground ? Color.secondary.opacity(0.18) : Color.clear,
                    lineWidth: 1
                )
        )
    }

    private func seriesToggleRow(condensedLayout: Bool) -> some View {
        HStack(spacing: condensedLayout ? 6 : 8) {
            seriesToggleButton(title: "Voltage", isOn: displayVoltage, condensedLayout: condensedLayout) {
                displayVoltage.toggle()
                if displayVoltage {
                    displayPower = false
                    displayEnergy = false
                    if displayTemperature && displayCurrent {
                        displayCurrent = false
                    }
                }
            }

            seriesToggleButton(title: "Current", isOn: displayCurrent, condensedLayout: condensedLayout) {
                displayCurrent.toggle()
                if displayCurrent {
                    displayPower = false
                    displayEnergy = false
                    if displayTemperature && displayVoltage {
                        displayVoltage = false
                    }
                }
            }

            seriesToggleButton(title: "Power", isOn: displayPower, condensedLayout: condensedLayout) {
                displayPower.toggle()
                if displayPower {
                    displayEnergy = false
                    displayCurrent = false
                    displayVoltage = false
                }
            }

            seriesToggleButton(title: "Energy", isOn: displayEnergy, condensedLayout: condensedLayout) {
                displayEnergy.toggle()
                if displayEnergy {
                    displayPower = false
                    displayCurrent = false
                    displayVoltage = false
                }
            }

            seriesToggleButton(title: "Temp", isOn: displayTemperature, condensedLayout: condensedLayout) {
                displayTemperature.toggle()
                if displayTemperature && displayVoltage && displayCurrent {
                    displayCurrent = false
                }
            }
        }
    }

    private func originControlsRow(
        voltageSeries: SeriesData,
        currentSeries: SeriesData,
        condensedLayout: Bool,
        showsLabel: Bool
    ) -> some View {
        HStack(spacing: usesCompactLandscapeOriginControls ? 6 : (condensedLayout ? 8 : 10)) {
            if supportsSharedOrigin {
                symbolControlChip(
                    systemImage: "equal.circle",
                    enabled: true,
                    active: useSharedOrigin,
                    condensedLayout: condensedLayout,
                    showsLabel: showsLabel,
                    label: "Match Y Scale",
                    accessibilityLabel: "Match Y scale"
                ) {
                    toggleSharedOrigin(
                        voltageSeries: voltageSeries,
                        currentSeries: currentSeries
                    )
                }
            }

            symbolControlChip(
                systemImage: pinOrigin ? "pin.fill" : "pin.slash",
                enabled: true,
                active: pinOrigin,
                condensedLayout: condensedLayout,
                showsLabel: showsLabel,
                label: pinOrigin ? "Origin Locked" : "Origin Auto",
                accessibilityLabel: pinOrigin ? "Unlock origin" : "Lock origin"
            ) {
                togglePinnedOrigin(
                    voltageSeries: voltageSeries,
                    currentSeries: currentSeries
                )
            }

            if !pinnedOriginIsZero {
                symbolControlChip(
                    systemImage: "0.circle",
                    enabled: true,
                    active: false,
                    condensedLayout: condensedLayout,
                    showsLabel: showsLabel,
                    label: "Origin 0",
                    accessibilityLabel: "Set origin to zero"
                ) {
                    setVisibleOriginsToZero()
                }
            }

            smoothingControlChip(
                condensedLayout: condensedLayout,
                showsLabel: showsLabel
            )

        }
    }

    private func smoothingControlChip(
        condensedLayout: Bool,
        showsLabel: Bool
    ) -> some View {
        Menu {
            ForEach(SmoothingLevel.allCases, id: \.self) { level in
                Button {
                    smoothingLevel = level
                } label: {
                    if smoothingLevel == level {
                        Label(level.label, systemImage: "checkmark")
                    } else {
                        Text(level.label)
                    }
                }
            }
        } label: {
            Group {
                if showsLabel {
                    VStack(alignment: .leading, spacing: 2) {
                        Label("Smoothing", systemImage: "waveform.path")
                            .font(controlChipFont(condensedLayout: condensedLayout))

                        Text(
                            smoothingLevel == .off
                            ? "Off"
                            : "MA \(smoothingLevel.movingAverageWindowSize)"
                        )
                        .font((condensedLayout ? Font.caption2 : .caption).weight(.semibold))
                        .foregroundColor(.secondary)
                        .monospacedDigit()
                    }
                    .padding(.horizontal, usesCompactLandscapeOriginControls ? 8 : (condensedLayout ? 10 : 12))
                    .padding(.vertical, usesCompactLandscapeOriginControls ? 6 : (condensedLayout ? 7 : (isLargeDisplay ? 9 : 8)))
                } else {
                    VStack(spacing: 1) {
                        Image(systemName: "waveform.path")
                            .font(.system(size: usesCompactLandscapeOriginControls ? 13 : (condensedLayout ? 15 : (isLargeDisplay ? 18 : 16)), weight: .semibold))

                        Text(smoothingLevel.shortLabel)
                            .font(.system(size: usesCompactLandscapeOriginControls ? 8 : 9, weight: .bold))
                            .monospacedDigit()
                    }
                    .frame(
                        width: usesCompactLandscapeOriginControls ? 36 : (condensedLayout ? 42 : (isLargeDisplay ? 50 : 44)),
                        height: usesCompactLandscapeOriginControls ? 34 : (condensedLayout ? 38 : (isLargeDisplay ? 48 : 42))
                    )
                }
            }
            .background(
                Capsule(style: .continuous)
                    .fill(smoothingLevel == .off ? Color.secondary.opacity(0.10) : Color.blue.opacity(0.14))
            )
            .overlay(
                Capsule(style: .continuous)
                    .stroke(
                        smoothingLevel == .off ? Color.secondary.opacity(0.18) : Color.blue.opacity(0.24),
                        lineWidth: 1
                    )
            )
        }
        .buttonStyle(.plain)
        .foregroundColor(smoothingLevel == .off ? .primary : .blue)
    }

    private func seriesToggleButton(
        title: String,
        isOn: Bool,
        condensedLayout: Bool,
        action: @escaping () -> Void
    ) -> some View {
        Button(action: action) {
            Text(title)
                .font(seriesToggleFont(condensedLayout: condensedLayout))
                .lineLimit(1)
                .minimumScaleFactor(0.82)
                .foregroundColor(isOn ? .white : .blue)
                .padding(.horizontal, condensedLayout ? 10 : (isLargeDisplay ? 16 : 12))
                .padding(.vertical, condensedLayout ? 7 : (isLargeDisplay ? 10 : 8))
                .frame(minWidth: condensedLayout ? 0 : (isLargeDisplay ? 104 : 84))
                .frame(maxWidth: stackedToolbarLayout ? .infinity : nil)
                .background(
                    RoundedRectangle(cornerRadius: condensedLayout ? 13 : 15, style: .continuous)
                        .fill(isOn ? Color.blue.opacity(0.88) : Color.blue.opacity(0.12))
                )
                .overlay(
                    RoundedRectangle(cornerRadius: condensedLayout ? 13 : 15, style: .continuous)
                        .stroke(Color.blue, lineWidth: 1.5)
                )
        }
        .buttonStyle(.plain)
    }

    private func symbolControlChip(
        systemImage: String,
        enabled: Bool,
        active: Bool,
        condensedLayout: Bool,
        showsLabel: Bool,
        label: String,
        accessibilityLabel: String,
        action: @escaping () -> Void
    ) -> some View {
        Button(action: {
            action()
        }) {
            Group {
                if showsLabel {
                    Label(label, systemImage: systemImage)
                        .font(controlChipFont(condensedLayout: condensedLayout))
                        .padding(.horizontal, usesCompactLandscapeOriginControls ? 8 : (condensedLayout ? 10 : 12))
                        .padding(.vertical, usesCompactLandscapeOriginControls ? 6 : (condensedLayout ? 7 : (isLargeDisplay ? 9 : 8)))
                } else {
                    Image(systemName: systemImage)
                        .font(.system(size: usesCompactLandscapeOriginControls ? 13 : (condensedLayout ? 15 : (isLargeDisplay ? 18 : 16)), weight: .semibold))
                        .frame(
                            width: usesCompactLandscapeOriginControls ? 30 : (condensedLayout ? 34 : (isLargeDisplay ? 42 : 38)),
                            height: usesCompactLandscapeOriginControls ? 30 : (condensedLayout ? 34 : (isLargeDisplay ? 42 : 38))
                        )
                }
            }
                .background(
                    Capsule(style: .continuous)
                        .fill(active ? Color.orange.opacity(0.18) : Color.secondary.opacity(0.10))
                )
        }
        .buttonStyle(.plain)
        .foregroundColor(enabled ? .primary : .secondary)
        .opacity(enabled ? 1 : 0.55)
        .accessibilityLabel(accessibilityLabel)
    }

    private func resetBuffer() {
        measurements.resetSeries()
    }

    private func seriesToggleFont(condensedLayout: Bool) -> Font {
        if isLargeDisplay {
            return .body.weight(.semibold)
        }
        return (condensedLayout ? Font.callout : .body).weight(.semibold)
    }

    private func controlChipFont(condensedLayout: Bool) -> Font {
        if isLargeDisplay {
            return .callout.weight(.semibold)
        }
        return (condensedLayout ? Font.callout : .footnote).weight(.semibold)
    }

    @ViewBuilder
    private func primaryAxisView(
        height: CGFloat,
        powerSeries: SeriesData,
        energySeries: SeriesData,
        voltageSeries: SeriesData,
        currentSeries: SeriesData
    ) -> some View {
        if displayPower {
            yAxisLabelsView(
                height: height,
                context: powerSeries.context,
                seriesKind: .power,
                measurementUnit: powerSeries.kind.unit,
                tint: powerSeries.kind.tint
            )
        } else if displayEnergy {
            yAxisLabelsView(
                height: height,
                context: energySeries.context,
                seriesKind: .energy,
                measurementUnit: energySeries.kind.unit,
                tint: energySeries.kind.tint
            )
        } else if displayVoltage {
            yAxisLabelsView(
                height: height,
                context: voltageSeries.context,
                seriesKind: .voltage,
                measurementUnit: voltageSeries.kind.unit,
                tint: voltageSeries.kind.tint
            )
        } else if displayCurrent {
            yAxisLabelsView(
                height: height,
                context: currentSeries.context,
                seriesKind: .current,
                measurementUnit: currentSeries.kind.unit,
                tint: currentSeries.kind.tint
            )
        }
    }

    @ViewBuilder
    private func renderedChart(
        powerSeries: SeriesData,
        energySeries: SeriesData,
        voltageSeries: SeriesData,
        currentSeries: SeriesData,
        temperatureSeries: SeriesData
    ) -> some View {
        if self.displayPower {
            Chart(points: powerSeries.points, context: powerSeries.context, strokeColor: .red)
                .opacity(0.72)
        } else if self.displayEnergy {
            Chart(points: energySeries.points, context: energySeries.context, strokeColor: .teal)
                .opacity(0.78)
        } else {
            if self.displayVoltage {
                Chart(points: voltageSeries.points, context: voltageSeries.context, strokeColor: .green)
                    .opacity(0.78)
            }
            if self.displayCurrent {
                Chart(points: currentSeries.points, context: currentSeries.context, strokeColor: .blue)
                    .opacity(0.78)
            }
        }

        if displayTemperature {
            Chart(points: temperatureSeries.points, context: temperatureSeries.context, strokeColor: .orange)
                .opacity(0.86)
        }
    }

    @ViewBuilder
    private func secondaryAxisView(
        height: CGFloat,
        powerSeries: SeriesData,
        energySeries: SeriesData,
        voltageSeries: SeriesData,
        currentSeries: SeriesData,
        temperatureSeries: SeriesData
    ) -> some View {
        if displayTemperature {
            yAxisLabelsView(
                height: height,
                context: temperatureSeries.context,
                seriesKind: .temperature,
                measurementUnit: measurementUnit(for: .temperature),
                tint: temperatureSeries.kind.tint
            )
        } else if displayVoltage && displayCurrent {
            yAxisLabelsView(
                height: height,
                context: currentSeries.context,
                seriesKind: .current,
                measurementUnit: currentSeries.kind.unit,
                tint: currentSeries.kind.tint
            )
        } else {
            primaryAxisView(
                height: height,
                powerSeries: powerSeries,
                energySeries: energySeries,
                voltageSeries: voltageSeries,
                currentSeries: currentSeries
            )
        }
    }

    private func displayedPrimarySeries(
        powerSeries: SeriesData,
        energySeries: SeriesData,
        voltageSeries: SeriesData,
        currentSeries: SeriesData
    ) -> SeriesData? {
        if displayPower {
            return powerSeries
        }
        if displayEnergy {
            return energySeries
        }
        if displayVoltage {
            return voltageSeries
        }
        if displayCurrent {
            return currentSeries
        }
        return nil
    }

    private func series(
        for measurement: Measurements.Measurement,
        kind: SeriesKind,
        minimumYSpan: Double,
        visibleTimeRange: ClosedRange<Date>? = nil
    ) -> SeriesData {
        let rawPoints = filteredPoints(
            measurement,
            visibleTimeRange: visibleTimeRange
        )
        let points = smoothedPoints(from: rawPoints)
        let samplePoints = points.filter { $0.isSample }
        let context = ChartContext()

        let autoBounds = automaticYBounds(
            for: samplePoints,
            minimumYSpan: minimumYSpan
        )
        let xBounds = xBounds(
            for: samplePoints,
            visibleTimeRange: visibleTimeRange
        )
        let lowerBound = resolvedLowerBound(
            for: kind,
            autoLowerBound: autoBounds.lowerBound
        )
        let upperBound = resolvedUpperBound(
            for: kind,
            lowerBound: lowerBound,
            autoUpperBound: autoBounds.upperBound,
            maximumSampleValue: samplePoints.map(\.value).max(),
            minimumYSpan: minimumYSpan
        )

        context.setBounds(
            xMin: CGFloat(xBounds.lowerBound.timeIntervalSince1970),
            xMax: CGFloat(xBounds.upperBound.timeIntervalSince1970),
            yMin: CGFloat(lowerBound),
            yMax: CGFloat(upperBound)
        )

        return SeriesData(
            kind: kind,
            points: points,
            samplePoints: samplePoints,
            context: context,
            autoLowerBound: autoBounds.lowerBound,
            autoUpperBound: autoBounds.upperBound,
            maximumSampleValue: samplePoints.map(\.value).max()
        )
    }

    private func overviewSeries(for kind: SeriesKind) -> SeriesData {
        series(
            for: measurement(for: kind),
            kind: kind,
            minimumYSpan: minimumYSpan(for: kind)
        )
    }

    private func smoothedPoints(
        from points: [Measurements.Measurement.Point]
    ) -> [Measurements.Measurement.Point] {
        guard smoothingLevel != .off else { return points }

        var smoothedPoints: [Measurements.Measurement.Point] = []
        var currentSegment: [Measurements.Measurement.Point] = []

        func flushCurrentSegment() {
            guard !currentSegment.isEmpty else { return }

            for point in smoothedSegment(currentSegment) {
                smoothedPoints.append(
                    Measurements.Measurement.Point(
                        id: smoothedPoints.count,
                        timestamp: point.timestamp,
                        value: point.value,
                        kind: .sample
                    )
                )
            }

            currentSegment.removeAll(keepingCapacity: true)
        }

        for point in points {
            if point.isDiscontinuity {
                flushCurrentSegment()

                if !smoothedPoints.isEmpty, smoothedPoints.last?.isDiscontinuity == false {
                    smoothedPoints.append(
                        Measurements.Measurement.Point(
                            id: smoothedPoints.count,
                            timestamp: point.timestamp,
                            value: smoothedPoints.last?.value ?? point.value,
                            kind: .discontinuity
                        )
                    )
                }
            } else {
                currentSegment.append(point)
            }
        }

        flushCurrentSegment()
        return smoothedPoints
    }

    private func smoothedSegment(
        _ segment: [Measurements.Measurement.Point]
    ) -> [Measurements.Measurement.Point] {
        let windowSize = smoothingLevel.movingAverageWindowSize
        guard windowSize > 1, segment.count > 2 else { return segment }

        let radius = windowSize / 2
        var prefixSums: [Double] = [0]
        prefixSums.reserveCapacity(segment.count + 1)

        for point in segment {
            prefixSums.append(prefixSums[prefixSums.count - 1] + point.value)
        }

        return segment.enumerated().map { index, point in
            let lowerBound = max(0, index - radius)
            let upperBound = min(segment.count - 1, index + radius)
            let sum = prefixSums[upperBound + 1] - prefixSums[lowerBound]
            let average = sum / Double(upperBound - lowerBound + 1)

            return Measurements.Measurement.Point(
                id: point.id,
                timestamp: point.timestamp,
                value: average,
                kind: .sample
            )
        }
    }

    private func measurement(for kind: SeriesKind) -> Measurements.Measurement {
        switch kind {
        case .power:
            return measurements.power
        case .energy:
            return measurements.energy
        case .voltage:
            return measurements.voltage
        case .current:
            return measurements.current
        case .temperature:
            return measurements.temperature
        }
    }

    private func minimumYSpan(for kind: SeriesKind) -> Double {
        switch kind {
        case .power:
            return minimumPowerSpan
        case .energy:
            return minimumEnergySpan
        case .voltage:
            return minimumVoltageSpan
        case .current:
            return minimumCurrentSpan
        case .temperature:
            return minimumTemperatureSpan
        }
    }

    private var supportsSharedOrigin: Bool {
        displayVoltage && displayCurrent && !displayPower && !displayEnergy
    }

    private var minimumSharedScaleSpan: Double {
        max(minimumVoltageSpan, minimumCurrentSpan)
    }

    private var pinnedOriginIsZero: Bool {
        if useSharedOrigin && supportsSharedOrigin {
            return pinOrigin && sharedAxisOrigin == 0
        }

        if displayPower {
            return pinOrigin && powerAxisOrigin == 0
        }

        if displayEnergy {
            return pinOrigin && energyAxisOrigin == 0
        }

        let visibleOrigins = [
            displayVoltage ? voltageAxisOrigin : nil,
            displayCurrent ? currentAxisOrigin : nil
        ]
        .compactMap { $0 }

        guard !visibleOrigins.isEmpty else { return false }
        return pinOrigin && visibleOrigins.allSatisfy { $0 == 0 }
    }

    private func toggleSharedOrigin(
        voltageSeries: SeriesData,
        currentSeries: SeriesData
    ) {
        guard supportsSharedOrigin else { return }

        if useSharedOrigin {
            useSharedOrigin = false
            return
        }

        captureCurrentOrigins(
            voltageSeries: voltageSeries,
            currentSeries: currentSeries
        )
        sharedAxisOrigin = min(voltageAxisOrigin, currentAxisOrigin)
        sharedAxisUpperBound = max(voltageSeries.autoUpperBound, currentSeries.autoUpperBound)
        ensureSharedScaleSpan()
        useSharedOrigin = true
        pinOrigin = true
    }

    private func togglePinnedOrigin(
        voltageSeries: SeriesData,
        currentSeries: SeriesData
    ) {
        if pinOrigin {
            pinOrigin = false
            return
        }

        captureCurrentOrigins(
            voltageSeries: voltageSeries,
            currentSeries: currentSeries
        )
        pinOrigin = true
    }

    private func setVisibleOriginsToZero() {
        if useSharedOrigin && supportsSharedOrigin {
            let currentSpan = max(sharedAxisUpperBound - sharedAxisOrigin, minimumSharedScaleSpan)
            sharedAxisOrigin = 0
            sharedAxisUpperBound = currentSpan
            voltageAxisOrigin = 0
            currentAxisOrigin = 0
            ensureSharedScaleSpan()
        } else {
            if displayPower {
                powerAxisOrigin = 0
            }
            if displayEnergy {
                energyAxisOrigin = 0
            }
            if displayVoltage {
                voltageAxisOrigin = 0
            }
            if displayCurrent {
                currentAxisOrigin = 0
            }
            if displayTemperature {
                temperatureAxisOrigin = 0
            }
        }

        pinOrigin = true
    }

    private func captureCurrentOrigins(
        voltageSeries: SeriesData,
        currentSeries: SeriesData
    ) {
        powerAxisOrigin = displayedLowerBoundForSeries(.power)
        energyAxisOrigin = displayedLowerBoundForSeries(.energy)
        voltageAxisOrigin = voltageSeries.autoLowerBound
        currentAxisOrigin = currentSeries.autoLowerBound
        temperatureAxisOrigin = displayedLowerBoundForSeries(.temperature)
        sharedAxisOrigin = min(voltageAxisOrigin, currentAxisOrigin)
        sharedAxisUpperBound = max(voltageSeries.autoUpperBound, currentSeries.autoUpperBound)
        ensureSharedScaleSpan()
    }

    private func displayedLowerBoundForSeries(_ kind: SeriesKind) -> Double {
        let visibleTimeRange = activeVisibleTimeRange

        switch kind {
        case .power:
            return pinOrigin
                ? powerAxisOrigin
                : automaticYBounds(
                    for: filteredSamplePoints(
                        measurements.power,
                        visibleTimeRange: visibleTimeRange
                    ),
                    minimumYSpan: minimumPowerSpan
                ).lowerBound
        case .energy:
            return pinOrigin
                ? energyAxisOrigin
                : automaticYBounds(
                    for: filteredSamplePoints(
                        measurements.energy,
                        visibleTimeRange: visibleTimeRange
                    ),
                    minimumYSpan: minimumEnergySpan
                ).lowerBound
        case .voltage:
            if pinOrigin && useSharedOrigin && supportsSharedOrigin {
                return sharedAxisOrigin
            }
            return pinOrigin
                ? voltageAxisOrigin
                : automaticYBounds(
                    for: filteredSamplePoints(
                        measurements.voltage,
                        visibleTimeRange: visibleTimeRange
                    ),
                    minimumYSpan: minimumVoltageSpan
                ).lowerBound
        case .current:
            if pinOrigin && useSharedOrigin && supportsSharedOrigin {
                return sharedAxisOrigin
            }
            return pinOrigin
                ? currentAxisOrigin
                : automaticYBounds(
                    for: filteredSamplePoints(
                        measurements.current,
                        visibleTimeRange: visibleTimeRange
                    ),
                    minimumYSpan: minimumCurrentSpan
                ).lowerBound
        case .temperature:
            return pinOrigin
                ? temperatureAxisOrigin
                : automaticYBounds(
                    for: filteredSamplePoints(
                        measurements.temperature,
                        visibleTimeRange: visibleTimeRange
                    ),
                    minimumYSpan: minimumTemperatureSpan
                ).lowerBound
        }
    }

    private var activeVisibleTimeRange: ClosedRange<Date>? {
        resolvedVisibleTimeRange(within: availableSelectionTimeRange())
    }

    private func filteredPoints(
        _ measurement: Measurements.Measurement,
        visibleTimeRange: ClosedRange<Date>? = nil
    ) -> [Measurements.Measurement.Point] {
        let resolvedRange: ClosedRange<Date>?

        switch (timeRange, visibleTimeRange) {
        case let (baseRange?, visibleRange?):
            let lowerBound = max(baseRange.lowerBound, visibleRange.lowerBound)
            let upperBound = min(baseRange.upperBound, visibleRange.upperBound)
            resolvedRange = lowerBound <= upperBound ? lowerBound...upperBound : nil
        case let (baseRange?, nil):
            resolvedRange = baseRange
        case let (nil, visibleRange?):
            resolvedRange = visibleRange
        case (nil, nil):
            resolvedRange = nil
        }

        guard let resolvedRange else {
            return timeRange == nil && visibleTimeRange == nil ? measurement.points : []
        }

        return measurement.points(in: resolvedRange)
    }

    private func filteredSamplePoints(
        _ measurement: Measurements.Measurement,
        visibleTimeRange: ClosedRange<Date>? = nil
    ) -> [Measurements.Measurement.Point] {
        filteredPoints(measurement, visibleTimeRange: visibleTimeRange).filter { point in
            point.isSample
        }
    }

    private func xBounds(
        for samplePoints: [Measurements.Measurement.Point],
        visibleTimeRange: ClosedRange<Date>? = nil
    ) -> ClosedRange<Date> {
        if let visibleTimeRange {
            return normalizedTimeRange(visibleTimeRange)
        }

        if let timeRange {
            return normalizedTimeRange(timeRange)
        }

        let upperBound = max(samplePoints.last?.timestamp ?? chartNow, chartNow)
        let lowerBound = samplePoints.first?.timestamp ?? upperBound.addingTimeInterval(-defaultEmptyChartTimeSpan)

        return normalizedTimeRange(lowerBound...upperBound)
    }

    private func availableSelectionTimeRange() -> ClosedRange<Date>? {
        if let timeRange {
            return normalizedTimeRange(timeRange)
        }

        let samplePoints = timelineSamplePoints()
        guard let lowerBound = samplePoints.first?.timestamp else {
            return nil
        }

        let upperBound = max(samplePoints.last?.timestamp ?? chartNow, chartNow)
        return normalizedTimeRange(lowerBound...upperBound)
    }

    private func timelineSamplePoints() -> [Measurements.Measurement.Point] {
        let candidates = [
            filteredSamplePoints(measurements.power),
            filteredSamplePoints(measurements.energy),
            filteredSamplePoints(measurements.voltage),
            filteredSamplePoints(measurements.current),
            filteredSamplePoints(measurements.temperature)
        ]

        return candidates.first(where: { !$0.isEmpty }) ?? []
    }

    private func resolvedVisibleTimeRange(
        within availableTimeRange: ClosedRange<Date>?
    ) -> ClosedRange<Date>? {
        guard let availableTimeRange else { return nil }
        guard let selectedVisibleTimeRange else { return availableTimeRange }

        if isPinnedToPresent {
            let pinnedRange: ClosedRange<Date>

            switch presentTrackingMode {
            case .keepDuration:
                let selectionSpan = selectedVisibleTimeRange.upperBound.timeIntervalSince(selectedVisibleTimeRange.lowerBound)
                pinnedRange = availableTimeRange.upperBound.addingTimeInterval(-selectionSpan)...availableTimeRange.upperBound
            case .keepStartTimestamp:
                pinnedRange = selectedVisibleTimeRange.lowerBound...availableTimeRange.upperBound
            }

            return clampedTimeRange(pinnedRange, within: availableTimeRange)
        }

        return clampedTimeRange(selectedVisibleTimeRange, within: availableTimeRange)
    }

    private func clampedTimeRange(
        _ candidateRange: ClosedRange<Date>,
        within bounds: ClosedRange<Date>
    ) -> ClosedRange<Date> {
        let normalizedBounds = normalizedTimeRange(bounds)
        let boundsSpan = normalizedBounds.upperBound.timeIntervalSince(normalizedBounds.lowerBound)

        guard boundsSpan > 0 else {
            return normalizedBounds
        }

        let minimumSpan = min(max(minimumTimeSpan, 0.1), boundsSpan)
        let requestedSpan = min(
            max(candidateRange.upperBound.timeIntervalSince(candidateRange.lowerBound), minimumSpan),
            boundsSpan
        )

        if requestedSpan >= boundsSpan {
            return normalizedBounds
        }

        var lowerBound = max(candidateRange.lowerBound, normalizedBounds.lowerBound)
        var upperBound = min(candidateRange.upperBound, normalizedBounds.upperBound)

        if upperBound.timeIntervalSince(lowerBound) < requestedSpan {
            if lowerBound == normalizedBounds.lowerBound {
                upperBound = lowerBound.addingTimeInterval(requestedSpan)
            } else {
                lowerBound = upperBound.addingTimeInterval(-requestedSpan)
            }
        }

        if upperBound > normalizedBounds.upperBound {
            let delta = upperBound.timeIntervalSince(normalizedBounds.upperBound)
            upperBound = normalizedBounds.upperBound
            lowerBound = lowerBound.addingTimeInterval(-delta)
        }

        if lowerBound < normalizedBounds.lowerBound {
            let delta = normalizedBounds.lowerBound.timeIntervalSince(lowerBound)
            lowerBound = normalizedBounds.lowerBound
            upperBound = upperBound.addingTimeInterval(delta)
        }

        return lowerBound...upperBound
    }

    private func normalizedTimeRange(_ range: ClosedRange<Date>) -> ClosedRange<Date> {
        let span = range.upperBound.timeIntervalSince(range.lowerBound)
        guard span < minimumTimeSpan else { return range }

        let expansion = (minimumTimeSpan - span) / 2
        return range.lowerBound.addingTimeInterval(-expansion)...range.upperBound.addingTimeInterval(expansion)
    }

    private func shouldShowRangeSelector(
        availableTimeRange: ClosedRange<Date>,
        series: SeriesData
    ) -> Bool {
        series.samplePoints.count > 1 &&
        availableTimeRange.upperBound.timeIntervalSince(availableTimeRange.lowerBound) > minimumTimeSpan
    }

    private func automaticYBounds(
        for samplePoints: [Measurements.Measurement.Point],
        minimumYSpan: Double
    ) -> (lowerBound: Double, upperBound: Double) {
        let negativeAllowance = max(0.05, minimumYSpan * 0.08)

        guard
            let minimumSampleValue = samplePoints.map(\.value).min(),
            let maximumSampleValue = samplePoints.map(\.value).max()
        else {
            return (0, minimumYSpan)
        }

        var lowerBound = minimumSampleValue
        var upperBound = maximumSampleValue
        let currentSpan = upperBound - lowerBound

        if currentSpan < minimumYSpan {
            let expansion = (minimumYSpan - currentSpan) / 2
            lowerBound -= expansion
            upperBound += expansion
        }

        if minimumSampleValue >= 0, lowerBound < -negativeAllowance {
            let shift = -negativeAllowance - lowerBound
            lowerBound += shift
            upperBound += shift
        }

        let snappedLowerBound = snappedOriginValue(lowerBound)
        let resolvedUpperBound = max(upperBound, snappedLowerBound + minimumYSpan)
        return (snappedLowerBound, resolvedUpperBound)
    }

    private func resolvedLowerBound(
        for kind: SeriesKind,
        autoLowerBound: Double
    ) -> Double {
        guard pinOrigin else { return autoLowerBound }

        if useSharedOrigin && supportsSharedOrigin && (kind == .voltage || kind == .current) {
            return sharedAxisOrigin
        }

        switch kind {
        case .power:
            return powerAxisOrigin
        case .energy:
            return energyAxisOrigin
        case .voltage:
            return voltageAxisOrigin
        case .current:
            return currentAxisOrigin
        case .temperature:
            return temperatureAxisOrigin
        }
    }

    private func resolvedUpperBound(
        for kind: SeriesKind,
        lowerBound: Double,
        autoUpperBound: Double,
        maximumSampleValue: Double?,
        minimumYSpan: Double
    ) -> Double {
        guard pinOrigin else {
            return autoUpperBound
        }

        if useSharedOrigin && supportsSharedOrigin && (kind == .voltage || kind == .current) {
            return max(sharedAxisUpperBound, sharedAxisOrigin + minimumSharedScaleSpan)
        }

        if kind == .temperature {
            return autoUpperBound
        }

        return max(
            maximumSampleValue ?? lowerBound,
            lowerBound + minimumYSpan,
            autoUpperBound
        )
    }

    private func applyOriginDelta(_ delta: Double, kind: SeriesKind) {
        let baseline = displayedLowerBoundForSeries(kind)
        let proposedOrigin = snappedOriginValue(baseline + delta)

        if useSharedOrigin && supportsSharedOrigin && (kind == .voltage || kind == .current) {
            let currentSpan = max(sharedAxisUpperBound - sharedAxisOrigin, minimumSharedScaleSpan)
            sharedAxisOrigin = min(proposedOrigin, maximumVisibleSharedOrigin())
            sharedAxisUpperBound = sharedAxisOrigin + currentSpan
            ensureSharedScaleSpan()
        } else {
            switch kind {
            case .power:
                powerAxisOrigin = min(proposedOrigin, maximumVisibleOrigin(for: .power))
            case .energy:
                energyAxisOrigin = min(proposedOrigin, maximumVisibleOrigin(for: .energy))
            case .voltage:
                voltageAxisOrigin = min(proposedOrigin, maximumVisibleOrigin(for: .voltage))
            case .current:
                currentAxisOrigin = min(proposedOrigin, maximumVisibleOrigin(for: .current))
            case .temperature:
                temperatureAxisOrigin = min(proposedOrigin, maximumVisibleOrigin(for: .temperature))
            }
        }

        pinOrigin = true
    }

    private func clearOriginOffset(for kind: SeriesKind) {
        if useSharedOrigin && supportsSharedOrigin && (kind == .voltage || kind == .current) {
            let currentSpan = max(sharedAxisUpperBound - sharedAxisOrigin, minimumSharedScaleSpan)
            sharedAxisOrigin = 0
            sharedAxisUpperBound = currentSpan
            ensureSharedScaleSpan()
            voltageAxisOrigin = 0
            currentAxisOrigin = 0
        } else {
            switch kind {
            case .power:
                powerAxisOrigin = 0
            case .energy:
                energyAxisOrigin = 0
            case .voltage:
                voltageAxisOrigin = 0
            case .current:
                currentAxisOrigin = 0
            case .temperature:
                temperatureAxisOrigin = 0
            }
        }

        pinOrigin = true
    }

    private func handleAxisTap(locationY: CGFloat, totalHeight: CGFloat, kind: SeriesKind) {
        guard totalHeight > 1 else { return }

        let normalized = max(0, min(1, locationY / totalHeight))
        if normalized < (1.0 / 3.0) {
            applyOriginDelta(-1, kind: kind)
        } else if normalized < (2.0 / 3.0) {
            clearOriginOffset(for: kind)
        } else {
            applyOriginDelta(1, kind: kind)
        }
    }

    private func maximumVisibleOrigin(for kind: SeriesKind) -> Double {
        let visibleTimeRange = activeVisibleTimeRange

        switch kind {
        case .power:
            return snappedOriginValue(
                filteredSamplePoints(
                    measurements.power,
                    visibleTimeRange: visibleTimeRange
                ).map(\.value).min() ?? 0
            )
        case .energy:
            return snappedOriginValue(
                filteredSamplePoints(
                    measurements.energy,
                    visibleTimeRange: visibleTimeRange
                ).map(\.value).min() ?? 0
            )
        case .voltage:
            return snappedOriginValue(
                filteredSamplePoints(
                    measurements.voltage,
                    visibleTimeRange: visibleTimeRange
                ).map(\.value).min() ?? 0
            )
        case .current:
            return snappedOriginValue(
                filteredSamplePoints(
                    measurements.current,
                    visibleTimeRange: visibleTimeRange
                ).map(\.value).min() ?? 0
            )
        case .temperature:
            return snappedOriginValue(
                filteredSamplePoints(
                    measurements.temperature,
                    visibleTimeRange: visibleTimeRange
                ).map(\.value).min() ?? 0
            )
        }
    }

    private func maximumVisibleSharedOrigin() -> Double {
        min(
            maximumVisibleOrigin(for: .voltage),
            maximumVisibleOrigin(for: .current)
        )
    }

    private func measurementUnit(for kind: SeriesKind) -> String {
        switch kind {
        case .temperature:
            let locale = Locale.autoupdatingCurrent
            if #available(iOS 16.0, *) {
                switch locale.measurementSystem {
                case .us:
                    return "°F"
                default:
                    return "°C"
                }
            }

            let regionCode = locale.regionCode ?? ""
            let fahrenheitRegions: Set<String> = ["US", "BS", "BZ", "KY", "PW", "LR", "FM", "MH"]
            return fahrenheitRegions.contains(regionCode) ? "°F" : "°C"
        default:
            return kind.unit
        }
    }

    private func ensureSharedScaleSpan() {
        sharedAxisUpperBound = max(sharedAxisUpperBound, sharedAxisOrigin + minimumSharedScaleSpan)
    }

    private func snappedOriginValue(_ value: Double) -> Double {
        if value >= 0 {
            return value.rounded(.down)
        }

        return value.rounded(.up)
    }

    private func trimBufferToSelection(_ range: ClosedRange<Date>) {
        measurements.keepOnly(in: range)
        selectedVisibleTimeRange = nil
        isPinnedToPresent = false
    }

    private func removeSelectionFromBuffer(_ range: ClosedRange<Date>) {
        measurements.removeValues(in: range)
        selectedVisibleTimeRange = nil
        isPinnedToPresent = false
    }

    private func yGuidePosition(
        for labelIndex: Int,
        context: ChartContext,
        height: CGFloat
    ) -> CGFloat {
        let value = context.yAxisLabel(for: labelIndex, of: yLabels)
        let anchorPoint = CGPoint(x: context.origin.x, y: CGFloat(value))
        return context.placeInRect(point: anchorPoint).y * height
    }

    private func xGuidePosition(
        for labelIndex: Int,
        context: ChartContext,
        width: CGFloat
    ) -> CGFloat {
        let value = context.xAxisLabel(for: labelIndex, of: xLabels)
        let anchorPoint = CGPoint(x: CGFloat(value), y: context.origin.y)
        return context.placeInRect(point: anchorPoint).x * width
    }
    
    // MARK: Cu iteratie nu functioneaza deoarece view-ul din bucla nu este reimprospatat la modificare obiectului observat
    fileprivate func xAxisLabelsView(
        context: ChartContext
    ) -> some View {
        var timeFormat: String?
        switch context.size.width {
        case 0..<3600: timeFormat = "HH:mm:ss"
        case 3600...86400: timeFormat = "HH:mm"
        default: timeFormat = "E HH:mm"
        }
        let labels = (1...xLabels).map {
            Date(timeIntervalSince1970: context.xAxisLabel(for: $0, of: self.xLabels)).format(as: timeFormat!)
        }
        let axisLabelFont: Font = {
            if isIPhone && isPortraitLayout {
                return .caption2.weight(.semibold)
            }
            return (isLargeDisplay ? Font.callout : .caption).weight(.semibold)
        }()

        return HStack(spacing: chartSectionSpacing) {
            Color.clear
                .frame(width: axisColumnWidth)

            GeometryReader { geometry in
                let labelWidth = max(
                    geometry.size.width / CGFloat(max(xLabels - 1, 1)),
                    1
                )

                ZStack(alignment: .topLeading) {
                    Path { path in
                        for labelIndex in 1...self.xLabels {
                            let x = xGuidePosition(
                                for: labelIndex,
                                context: context,
                                width: geometry.size.width
                            )
                            path.move(to: CGPoint(x: x, y: 0))
                            path.addLine(to: CGPoint(x: x, y: 6))
                        }
                    }
                    .stroke(Color.secondary.opacity(0.26), lineWidth: 0.75)

                    ForEach(Array(labels.enumerated()), id: \.offset) { item in
                        let labelIndex = item.offset + 1
                        let centerX = xGuidePosition(
                            for: labelIndex,
                            context: context,
                            width: geometry.size.width
                        )

                        Text(item.element)
                            .font(axisLabelFont)
                            .monospacedDigit()
                            .lineLimit(1)
                            .minimumScaleFactor(0.74)
                            .frame(width: labelWidth)
                            .position(
                                x: centerX,
                                y: geometry.size.height * 0.7
                            )
                    }
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
            }

            Color.clear
                .frame(width: axisColumnWidth)
        }
    }
    
    private func yAxisLabelsView(
        height: CGFloat,
        context: ChartContext,
        seriesKind: SeriesKind,
        measurementUnit: String,
        tint: Color
    ) -> some View {
        let yAxisFont: Font = {
            if isIPhone && isPortraitLayout {
                return .caption2.weight(.semibold)
            }
            return (isLargeDisplay ? Font.callout : .footnote).weight(.semibold)
        }()

        let unitFont: Font = {
            if isIPhone && isPortraitLayout {
                return .caption2.weight(.bold)
            }
            return (isLargeDisplay ? Font.footnote : .caption2).weight(.bold)
        }()

        return GeometryReader { geometry in
            let footerHeight: CGFloat = isLargeDisplay ? 30 : 24
            let topInset: CGFloat = isLargeDisplay ? 34 : 28
            let labelAreaHeight = max(geometry.size.height - footerHeight - topInset, 1)

            ZStack(alignment: .top) {
                ForEach(0..<yLabels, id: \.self) { row in
                    let labelIndex = yLabels - row

                    Text("\(context.yAxisLabel(for: labelIndex, of: yLabels).format(fractionDigits: 1))")
                        .font(yAxisFont)
                        .monospacedDigit()
                        .lineLimit(1)
                        .minimumScaleFactor(0.8)
                        .frame(width: max(geometry.size.width - 10, 0))
                        .position(
                            x: geometry.size.width / 2,
                            y: topInset + yGuidePosition(
                                for: labelIndex,
                                context: context,
                                height: labelAreaHeight
                            )
                        )
                }

                Text(measurementUnit)
                    .font(unitFont)
                    .foregroundColor(tint)
                    .padding(.horizontal, isLargeDisplay ? 8 : 6)
                    .padding(.vertical, isLargeDisplay ? 5 : 4)
                    .background(
                        Capsule(style: .continuous)
                            .fill(tint.opacity(0.14))
                    )
                    .padding(.top, 8)

            }
        }
        .frame(height: height)
        .background(
            RoundedRectangle(cornerRadius: 16, style: .continuous)
                .fill(tint.opacity(0.12))
        )
        .overlay(
            RoundedRectangle(cornerRadius: 16, style: .continuous)
                .stroke(tint.opacity(0.20), lineWidth: 1)
        )
        .contentShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
        .gesture(
            DragGesture(minimumDistance: 0)
                .onEnded { value in
                    handleAxisTap(locationY: value.location.y, totalHeight: height, kind: seriesKind)
                }
        )
    }
    
    fileprivate func horizontalGuides(context: ChartContext) -> some View {
        GeometryReader { geometry in
            Path { path in
                for labelIndex in 1...self.yLabels {
                    let y = yGuidePosition(
                        for: labelIndex,
                        context: context,
                        height: geometry.size.height
                    )
                    path.addLine(from: CGPoint(x: 0, y: y), to: CGPoint(x: geometry.size.width, y: y))
                }
            }
            .stroke(Color.secondary.opacity(0.38), lineWidth: 0.85)
        }
    }
    
    fileprivate func verticalGuides(context: ChartContext) -> some View {
        GeometryReader { geometry in
            Path { path in
                
                for labelIndex in 2..<self.xLabels {
                    let x = xGuidePosition(
                        for: labelIndex,
                        context: context,
                        width: geometry.size.width
                    )
                    path.move(to: CGPoint(x: x, y: 0) )
                    path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height) )
                }
            }
            .stroke(Color.secondary.opacity(0.34), style: StrokeStyle(lineWidth: 0.8, dash: [4, 4]))
        }
    }

    fileprivate func discontinuityMarkers(
        points: [Measurements.Measurement.Point],
        context: ChartContext
    ) -> some View {
        GeometryReader { geometry in
            Path { path in
                for point in points where point.isDiscontinuity {
                    let markerX = context.placeInRect(
                        point: CGPoint(
                            x: point.timestamp.timeIntervalSince1970,
                            y: context.origin.y
                        )
                    ).x * geometry.size.width
                    path.move(to: CGPoint(x: markerX, y: 0))
                    path.addLine(to: CGPoint(x: markerX, y: geometry.size.height))
                }
            }
            .stroke(Color.orange.opacity(0.75), style: StrokeStyle(lineWidth: 1, dash: [5, 4]))
        }
    }
    
}

private struct TimeRangeSelectorView: View {
    private enum DragTarget {
        case lowerBound
        case upperBound
        case window
    }

    private enum ActionTone {
        case reversible
        case destructive
        case destructiveProminent
    }

    private struct DragState {
        let target: DragTarget
        let initialRange: ClosedRange<Date>
    }

    let points: [Measurements.Measurement.Point]
    let context: ChartContext
    let availableTimeRange: ClosedRange<Date>
    let selectorTint: Color
    let compactLayout: Bool
    let minimumSelectionSpan: TimeInterval
    let onKeepSelection: (ClosedRange<Date>) -> Void
    let onRemoveSelection: (ClosedRange<Date>) -> Void
    let onResetBuffer: () -> Void

    @Binding var selectedTimeRange: ClosedRange<Date>?
    @Binding var isPinnedToPresent: Bool
    @Binding var presentTrackingMode: PresentTrackingMode
    @State private var dragState: DragState?
    @State private var showResetConfirmation: Bool = false

    private var totalSpan: TimeInterval {
        availableTimeRange.upperBound.timeIntervalSince(availableTimeRange.lowerBound)
    }

    private var currentRange: ClosedRange<Date> {
        resolvedSelectionRange()
    }

    private var trackHeight: CGFloat {
        compactLayout ? 72 : 86
    }

    private var cornerRadius: CGFloat {
        compactLayout ? 14 : 16
    }

    private var boundaryFont: Font {
        compactLayout ? .caption.weight(.semibold) : .footnote.weight(.semibold)
    }

    private var symbolButtonSize: CGFloat {
        compactLayout ? 28 : 32
    }

    var body: some View {
        let coversFullRange = selectionCoversFullRange(currentRange)

        VStack(alignment: .leading, spacing: compactLayout ? 6 : 8) {
            if !coversFullRange || isPinnedToPresent {
                HStack(spacing: 8) {
                    alignmentButton(
                        systemName: "arrow.left.to.line.compact",
                        isActive: currentRange.lowerBound == availableTimeRange.lowerBound && !isPinnedToPresent,
                        action: alignSelectionToLeadingEdge,
                        accessibilityLabel: "Align selection to start"
                    )

                    alignmentButton(
                        systemName: "arrow.right.to.line.compact",
                        isActive: isPinnedToPresent || selectionTouchesPresent(currentRange),
                        action: alignSelectionToTrailingEdge,
                        accessibilityLabel: "Align selection to present"
                    )

                    Spacer(minLength: 0)

                    if isPinnedToPresent {
                        trackingModeToggleButton()
                    }
                }
            }

            HStack(spacing: 8) {
                if !coversFullRange {
                    actionButton(
                        title: compactLayout ? "Keep" : "Keep Selection",
                        systemName: "scissors",
                        tone: .destructive,
                        action: {
                            onKeepSelection(currentRange)
                        }
                    )

                    actionButton(
                        title: compactLayout ? "Cut" : "Remove Selection",
                        systemName: "minus.circle",
                        tone: .destructive,
                        action: {
                            onRemoveSelection(currentRange)
                        }
                    )
                }

                Spacer(minLength: 0)

                actionButton(
                    title: compactLayout ? "Reset" : "Reset Buffer",
                    systemName: "trash",
                    tone: .destructiveProminent,
                    action: {
                        showResetConfirmation = true
                    }
                )
            }
            .confirmationDialog("Reset captured measurements?", isPresented: $showResetConfirmation, titleVisibility: .visible) {
                Button("Reset buffer", role: .destructive) {
                    onResetBuffer()
                }
                Button("Cancel", role: .cancel) {}
            }

            GeometryReader { geometry in
                let selectionFrame = selectionFrame(in: geometry.size)
                let dimmingColor = Color(uiColor: .systemBackground).opacity(0.58)

                ZStack(alignment: .topLeading) {
                    RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
                        .fill(Color.primary.opacity(0.05))

                    Chart(
                        points: points,
                        context: context,
                        areaChart: true,
                        strokeColor: selectorTint,
                        areaFillColor: selectorTint.opacity(0.22)
                    )
                    .opacity(0.94)
                    .allowsHitTesting(false)

                    Chart(
                        points: points,
                        context: context,
                        strokeColor: selectorTint.opacity(0.56)
                    )
                    .opacity(0.82)
                    .allowsHitTesting(false)

                    if selectionFrame.minX > 0 {
                        Rectangle()
                            .fill(dimmingColor)
                            .frame(width: selectionFrame.minX, height: geometry.size.height)
                            .allowsHitTesting(false)
                    }

                    if selectionFrame.maxX < geometry.size.width {
                        Rectangle()
                            .fill(dimmingColor)
                            .frame(
                                width: max(geometry.size.width - selectionFrame.maxX, 0),
                                height: geometry.size.height
                            )
                            .offset(x: selectionFrame.maxX)
                            .allowsHitTesting(false)
                    }

                    RoundedRectangle(cornerRadius: max(cornerRadius - 2, 10), style: .continuous)
                        .fill(selectorTint.opacity(0.18))
                        .frame(width: max(selectionFrame.width, 2), height: geometry.size.height)
                        .offset(x: selectionFrame.minX)
                        .allowsHitTesting(false)

                    RoundedRectangle(cornerRadius: max(cornerRadius - 2, 10), style: .continuous)
                        .stroke(selectorTint.opacity(0.52), lineWidth: 1.2)
                        .frame(width: max(selectionFrame.width, 2), height: geometry.size.height)
                        .offset(x: selectionFrame.minX)
                        .allowsHitTesting(false)

                    handleView(height: max(geometry.size.height - 18, 16))
                        .offset(x: max(selectionFrame.minX + 6, 6), y: 9)
                        .allowsHitTesting(false)

                    handleView(height: max(geometry.size.height - 18, 16))
                        .offset(x: max(selectionFrame.maxX - 12, 6), y: 9)
                        .allowsHitTesting(false)
                }
                .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
                .overlay(
                    RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
                        .stroke(Color.secondary.opacity(0.18), lineWidth: 1)
                )
                .contentShape(Rectangle())
                .gesture(selectionGesture(totalWidth: geometry.size.width))
            }
            .frame(height: trackHeight)

            HStack {
                Text(boundaryLabel(for: availableTimeRange.lowerBound))
                Spacer(minLength: 0)
                Text(boundaryLabel(for: availableTimeRange.upperBound))
            }
            .font(boundaryFont)
            .foregroundColor(.secondary)
            .monospacedDigit()
        }
    }

    private func handleView(height: CGFloat) -> some View {
        Capsule(style: .continuous)
            .fill(Color.white.opacity(0.95))
            .frame(width: 6, height: height)
            .shadow(color: .black.opacity(0.08), radius: 2, x: 0, y: 1)
    }

    private func alignmentButton(
        systemName: String,
        isActive: Bool,
        action: @escaping () -> Void,
        accessibilityLabel: String
    ) -> some View {
        Button(action: action) {
            Image(systemName: systemName)
                .font(.system(size: compactLayout ? 12 : 13, weight: .semibold))
                .frame(width: symbolButtonSize, height: symbolButtonSize)
        }
        .buttonStyle(.plain)
        .foregroundColor(isActive ? .white : selectorTint)
        .background(
            RoundedRectangle(cornerRadius: 9, style: .continuous)
                .fill(isActive ? selectorTint : selectorTint.opacity(0.14))
        )
        .overlay(
            RoundedRectangle(cornerRadius: 9, style: .continuous)
                .stroke(selectorTint.opacity(0.28), lineWidth: 1)
        )
        .accessibilityLabel(accessibilityLabel)
    }

    private func trackingModeToggleButton() -> some View {
        Button {
            presentTrackingMode = presentTrackingMode == .keepDuration
                ? .keepStartTimestamp
                : .keepDuration
        } label: {
            Image(systemName: trackingModeSymbolName)
                .font(.system(size: compactLayout ? 12 : 13, weight: .semibold))
                .frame(width: symbolButtonSize, height: symbolButtonSize)
        }
        .buttonStyle(.plain)
        .foregroundColor(.white)
        .background(
            RoundedRectangle(cornerRadius: 9, style: .continuous)
                .fill(selectorTint)
        )
        .overlay(
            RoundedRectangle(cornerRadius: 9, style: .continuous)
                .stroke(selectorTint.opacity(0.28), lineWidth: 1)
        )
        .accessibilityLabel(trackingModeAccessibilityLabel)
        .accessibilityHint("Toggles how the interval follows the present")
    }

    private func actionButton(
        title: String,
        systemName: String,
        tone: ActionTone,
        action: @escaping () -> Void
    ) -> some View {
        let foregroundColor: Color = {
            switch tone {
            case .reversible, .destructive:
                return toneColor(for: tone)
            case .destructiveProminent:
                return .white
            }
        }()

        return Button(action: action) {
            Label(title, systemImage: systemName)
                .font(compactLayout ? .caption.weight(.semibold) : .footnote.weight(.semibold))
                .padding(.horizontal, compactLayout ? 10 : 12)
                .padding(.vertical, compactLayout ? 7 : 8)
        }
        .buttonStyle(.plain)
        .foregroundColor(foregroundColor)
        .background(
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .fill(actionButtonBackground(for: tone))
        )
        .overlay(
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .stroke(actionButtonBorder(for: tone), lineWidth: 1)
        )
    }

    private func toneColor(for tone: ActionTone) -> Color {
        switch tone {
        case .reversible:
            return selectorTint
        case .destructive, .destructiveProminent:
            return .red
        }
    }

    private func actionButtonBackground(for tone: ActionTone) -> Color {
        switch tone {
        case .reversible:
            return selectorTint.opacity(0.12)
        case .destructive:
            return Color.red.opacity(0.12)
        case .destructiveProminent:
            return Color.red.opacity(0.82)
        }
    }

    private func actionButtonBorder(for tone: ActionTone) -> Color {
        switch tone {
        case .reversible:
            return selectorTint.opacity(0.22)
        case .destructive:
            return Color.red.opacity(0.22)
        case .destructiveProminent:
            return Color.red.opacity(0.72)
        }
    }

    private var trackingModeSymbolName: String {
        switch presentTrackingMode {
        case .keepDuration:
            return "arrow.left.and.right"
        case .keepStartTimestamp:
            return "arrow.left.to.line.compact"
        }
    }

    private var trackingModeAccessibilityLabel: String {
        switch presentTrackingMode {
        case .keepDuration:
            return "Follow present keeping span"
        case .keepStartTimestamp:
            return "Follow present keeping start"
        }
    }

    private func alignSelectionToLeadingEdge() {
        let alignedRange = normalizedSelectionRange(
            availableTimeRange.lowerBound...currentRange.upperBound
        )
        applySelection(alignedRange, pinToPresent: false)
    }

    private func alignSelectionToTrailingEdge() {
        let alignedRange = normalizedSelectionRange(
            currentRange.lowerBound...availableTimeRange.upperBound
        )
        applySelection(alignedRange, pinToPresent: true)
    }

    private func selectionGesture(totalWidth: CGFloat) -> some Gesture {
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                updateSelectionDrag(value: value, totalWidth: totalWidth)
            }
            .onEnded { _ in
                dragState = nil
            }
    }

    private func updateSelectionDrag(
        value: DragGesture.Value,
        totalWidth: CGFloat
    ) {
        let startingRange = resolvedSelectionRange()

        if dragState == nil {
            dragState = DragState(
                target: dragTarget(
                    for: value.startLocation.x,
                    selectionFrame: selectionFrame(for: startingRange, width: totalWidth)
                ),
                initialRange: startingRange
            )
        }

        guard let dragState else { return }

        let resultingRange = snappedToEdges(
            adjustedRange(
                from: dragState.initialRange,
                target: dragState.target,
                translationX: value.translation.width,
                totalWidth: totalWidth
            ),
            target: dragState.target,
            totalWidth: totalWidth
        )

        applySelection(
            resultingRange,
            pinToPresent: shouldKeepPresentPin(
                during: dragState.target,
                initialRange: dragState.initialRange,
                resultingRange: resultingRange
            ),
        )
    }

    private func dragTarget(
        for startX: CGFloat,
        selectionFrame: CGRect
    ) -> DragTarget {
        let handleZone: CGFloat = compactLayout ? 20 : 24

        if abs(startX - selectionFrame.minX) <= handleZone {
            return .lowerBound
        }

        if abs(startX - selectionFrame.maxX) <= handleZone {
            return .upperBound
        }

        if selectionFrame.contains(CGPoint(x: startX, y: selectionFrame.midY)) {
            return .window
        }

        return startX < selectionFrame.minX ? .lowerBound : .upperBound
    }

    private func adjustedRange(
        from initialRange: ClosedRange<Date>,
        target: DragTarget,
        translationX: CGFloat,
        totalWidth: CGFloat
    ) -> ClosedRange<Date> {
        guard totalSpan > 0, totalWidth > 0 else {
            return availableTimeRange
        }

        let delta = TimeInterval(translationX / totalWidth) * totalSpan
        let minimumSpan = min(max(minimumSelectionSpan, 0.1), totalSpan)

        switch target {
        case .lowerBound:
            let maximumLowerBound = initialRange.upperBound.addingTimeInterval(-minimumSpan)
            let newLowerBound = min(
                max(initialRange.lowerBound.addingTimeInterval(delta), availableTimeRange.lowerBound),
                maximumLowerBound
            )
            return normalizedSelectionRange(newLowerBound...initialRange.upperBound)

        case .upperBound:
            let minimumUpperBound = initialRange.lowerBound.addingTimeInterval(minimumSpan)
            let newUpperBound = max(
                min(initialRange.upperBound.addingTimeInterval(delta), availableTimeRange.upperBound),
                minimumUpperBound
            )
            return normalizedSelectionRange(initialRange.lowerBound...newUpperBound)

        case .window:
            let span = initialRange.upperBound.timeIntervalSince(initialRange.lowerBound)
            guard span < totalSpan else { return availableTimeRange }

            var lowerBound = initialRange.lowerBound.addingTimeInterval(delta)
            var upperBound = initialRange.upperBound.addingTimeInterval(delta)

            if lowerBound < availableTimeRange.lowerBound {
                upperBound = upperBound.addingTimeInterval(
                    availableTimeRange.lowerBound.timeIntervalSince(lowerBound)
                )
                lowerBound = availableTimeRange.lowerBound
            }

            if upperBound > availableTimeRange.upperBound {
                lowerBound = lowerBound.addingTimeInterval(
                    -upperBound.timeIntervalSince(availableTimeRange.upperBound)
                )
                upperBound = availableTimeRange.upperBound
            }

            return normalizedSelectionRange(lowerBound...upperBound)
        }
    }

    private func snappedToEdges(
        _ candidateRange: ClosedRange<Date>,
        target: DragTarget,
        totalWidth: CGFloat
    ) -> ClosedRange<Date> {
        guard totalSpan > 0 else {
            return availableTimeRange
        }

        let snapInterval = edgeSnapInterval(for: totalWidth)
        let selectionSpan = candidateRange.upperBound.timeIntervalSince(candidateRange.lowerBound)
        var lowerBound = candidateRange.lowerBound
        var upperBound = candidateRange.upperBound

        if target != .upperBound,
           lowerBound.timeIntervalSince(availableTimeRange.lowerBound) <= snapInterval {
            lowerBound = availableTimeRange.lowerBound
            if target == .window {
                upperBound = lowerBound.addingTimeInterval(selectionSpan)
            }
        }

        if target != .lowerBound,
           availableTimeRange.upperBound.timeIntervalSince(upperBound) <= snapInterval {
            upperBound = availableTimeRange.upperBound
            if target == .window {
                lowerBound = upperBound.addingTimeInterval(-selectionSpan)
            }
        }

        return normalizedSelectionRange(lowerBound...upperBound)
    }

    private func edgeSnapInterval(
        for totalWidth: CGFloat
    ) -> TimeInterval {
        guard totalWidth > 0 else { return minimumSelectionSpan }

        let snapWidth = min(
            max(compactLayout ? 18 : 22, totalWidth * 0.04),
            totalWidth * 0.18
        )
        let intervalFromWidth = TimeInterval(snapWidth / totalWidth) * totalSpan
        return min(
            max(intervalFromWidth, minimumSelectionSpan * 0.5),
            totalSpan / 4
        )
    }

    private func resolvedSelectionRange() -> ClosedRange<Date> {
        guard let selectedTimeRange else { return availableTimeRange }

        if isPinnedToPresent {
            switch presentTrackingMode {
            case .keepDuration:
                let selectionSpan = selectedTimeRange.upperBound.timeIntervalSince(selectedTimeRange.lowerBound)
                return normalizedSelectionRange(
                    availableTimeRange.upperBound.addingTimeInterval(-selectionSpan)...availableTimeRange.upperBound
                )
            case .keepStartTimestamp:
                return normalizedSelectionRange(
                    selectedTimeRange.lowerBound...availableTimeRange.upperBound
                )
            }
        }

        return normalizedSelectionRange(selectedTimeRange)
    }

    private func normalizedSelectionRange(
        _ candidateRange: ClosedRange<Date>
    ) -> ClosedRange<Date> {
        let availableSpan = totalSpan
        guard availableSpan > 0 else { return availableTimeRange }

        let minimumSpan = min(max(minimumSelectionSpan, 0.1), availableSpan)
        let requestedSpan = min(
            max(candidateRange.upperBound.timeIntervalSince(candidateRange.lowerBound), minimumSpan),
            availableSpan
        )

        if requestedSpan >= availableSpan {
            return availableTimeRange
        }

        var lowerBound = max(candidateRange.lowerBound, availableTimeRange.lowerBound)
        var upperBound = min(candidateRange.upperBound, availableTimeRange.upperBound)

        if upperBound.timeIntervalSince(lowerBound) < requestedSpan {
            if lowerBound == availableTimeRange.lowerBound {
                upperBound = lowerBound.addingTimeInterval(requestedSpan)
            } else {
                lowerBound = upperBound.addingTimeInterval(-requestedSpan)
            }
        }

        if upperBound > availableTimeRange.upperBound {
            let delta = upperBound.timeIntervalSince(availableTimeRange.upperBound)
            upperBound = availableTimeRange.upperBound
            lowerBound = lowerBound.addingTimeInterval(-delta)
        }

        if lowerBound < availableTimeRange.lowerBound {
            let delta = availableTimeRange.lowerBound.timeIntervalSince(lowerBound)
            lowerBound = availableTimeRange.lowerBound
            upperBound = upperBound.addingTimeInterval(delta)
        }

        return lowerBound...upperBound
    }

    private func shouldKeepPresentPin(
        during target: DragTarget,
        initialRange: ClosedRange<Date>,
        resultingRange: ClosedRange<Date>
    ) -> Bool {
        let startedPinnedToPresent =
            isPinnedToPresent ||
            selectionCoversFullRange(initialRange)

        guard startedPinnedToPresent else {
            return selectionTouchesPresent(resultingRange)
        }

        switch target {
        case .lowerBound:
            return true
        case .upperBound, .window:
            return selectionTouchesPresent(resultingRange)
        }
    }

    private func applySelection(
        _ candidateRange: ClosedRange<Date>,
        pinToPresent: Bool
    ) {
        let normalizedRange = normalizedSelectionRange(candidateRange)

        if selectionCoversFullRange(normalizedRange) && !pinToPresent {
            selectedTimeRange = nil
        } else {
            selectedTimeRange = normalizedRange
        }

        isPinnedToPresent = pinToPresent
    }

    private func selectionTouchesPresent(
        _ range: ClosedRange<Date>
    ) -> Bool {
        let tolerance = max(0.5, minimumSelectionSpan * 0.25)
        return abs(range.upperBound.timeIntervalSince(availableTimeRange.upperBound)) <= tolerance
    }

    private func selectionCoversFullRange(
        _ range: ClosedRange<Date>
    ) -> Bool {
        let tolerance = max(0.5, minimumSelectionSpan * 0.25)
        return abs(range.lowerBound.timeIntervalSince(availableTimeRange.lowerBound)) <= tolerance &&
        abs(range.upperBound.timeIntervalSince(availableTimeRange.upperBound)) <= tolerance
    }

    private func selectionFrame(in size: CGSize) -> CGRect {
        selectionFrame(for: currentRange, width: size.width)
    }

    private func selectionFrame(
        for range: ClosedRange<Date>,
        width: CGFloat
    ) -> CGRect {
        guard width > 0, totalSpan > 0 else {
            return CGRect(origin: .zero, size: CGSize(width: width, height: trackHeight))
        }

        let minimumX = xPosition(for: range.lowerBound, width: width)
        let maximumX = xPosition(for: range.upperBound, width: width)
        return CGRect(
            x: minimumX,
            y: 0,
            width: max(maximumX - minimumX, 2),
            height: trackHeight
        )
    }

    private func xPosition(for date: Date, width: CGFloat) -> CGFloat {
        guard width > 0, totalSpan > 0 else { return 0 }

        let offset = date.timeIntervalSince(availableTimeRange.lowerBound)
        let normalizedOffset = min(max(offset / totalSpan, 0), 1)
        return CGFloat(normalizedOffset) * width
    }

    private func boundaryLabel(for date: Date) -> String {
        date.format(as: boundaryDateFormat)
    }

    private var boundaryDateFormat: String {
        switch totalSpan {
        case 0..<86400:
            return "HH:mm"
        case 86400..<604800:
            return "MMM d HH:mm"
        default:
            return "MMM d"
        }
    }
}

struct Chart : View {
    
    @Environment(\.displayScale) private var displayScale

    let points: [Measurements.Measurement.Point]
    let context: ChartContext
    var areaChart: Bool = false
    var strokeColor: Color = .black
    var areaFillColor: Color? = nil
    
    var body : some View {
        GeometryReader { geometry in
            if self.areaChart {
                let fillColor = areaFillColor ?? strokeColor.opacity(0.2)
                self.path( geometry: geometry )
                    .fill(
                        LinearGradient(
                            gradient: .init(
                                colors: [
                                    fillColor.opacity(0.72),
                                    fillColor.opacity(0.18)
                                ]
                            ),
                            startPoint: .init(x: 0.5, y: 0.08),
                            endPoint: .init(x: 0.5, y: 0.92)
                        )
                    )
            } else {
                self.path( geometry: geometry )
                    .stroke(self.strokeColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
            }
        }
    }
    
    fileprivate func path(geometry: GeometryProxy) -> Path {
        let displayedPoints = scaledPoints(for: geometry.size.width)
        let baselineY = context.placeInRect(
            point: CGPoint(x: context.origin.x, y: context.origin.y)
        ).y * geometry.size.height

        return Path { path in
            var firstRenderedPoint: CGPoint?
            var lastRenderedPoint: CGPoint?
            var needsMove = true

            for point in displayedPoints {
                if point.isDiscontinuity {
                    closeAreaSegment(
                        in: &path,
                        firstPoint: firstRenderedPoint,
                        lastPoint: lastRenderedPoint,
                        baselineY: baselineY
                    )
                    firstRenderedPoint = nil
                    lastRenderedPoint = nil
                    needsMove = true
                    continue
                }

                let item = context.placeInRect(point: point.point())
                let renderedPoint = CGPoint(
                    x: item.x * geometry.size.width,
                    y: item.y * geometry.size.height
                )

                if needsMove {
                    path.move(to: renderedPoint)
                    firstRenderedPoint = renderedPoint
                    needsMove = false
                } else {
                    path.addLine(to: renderedPoint)
                }

                lastRenderedPoint = renderedPoint
            }

            closeAreaSegment(
                in: &path,
                firstPoint: firstRenderedPoint,
                lastPoint: lastRenderedPoint,
                baselineY: baselineY
            )
        }
    }

    private func closeAreaSegment(
        in path: inout Path,
        firstPoint: CGPoint?,
        lastPoint: CGPoint?,
        baselineY: CGFloat
    ) {
        guard areaChart, let firstPoint, let lastPoint, firstPoint != lastPoint else { return }

        path.addLine(to: CGPoint(x: lastPoint.x, y: baselineY))
        path.addLine(to: CGPoint(x: firstPoint.x, y: baselineY))
        path.closeSubpath()
    }

    private func scaledPoints(for width: CGFloat) -> [Measurements.Measurement.Point] {
        let sampleCount = points.reduce(into: 0) { partialResult, point in
            if point.isSample {
                partialResult += 1
            }
        }

        let displayColumns = max(Int((width * max(displayScale, 1)).rounded(.up)), 1)
        let maximumSamplesToRender = max(displayColumns * (areaChart ? 3 : 4), 240)

        guard sampleCount > maximumSamplesToRender, context.isValid else {
            return points
        }

        var scaledPoints: [Measurements.Measurement.Point] = []
        var currentSegment: [Measurements.Measurement.Point] = []

        for point in points {
            if point.isDiscontinuity {
                appendScaledSegment(currentSegment, to: &scaledPoints, displayColumns: displayColumns)
                currentSegment.removeAll(keepingCapacity: true)

                if !scaledPoints.isEmpty, scaledPoints.last?.isDiscontinuity == false {
                    appendScaledPoint(point, to: &scaledPoints)
                }
            } else {
                currentSegment.append(point)
            }
        }

        appendScaledSegment(currentSegment, to: &scaledPoints, displayColumns: displayColumns)
        return scaledPoints.isEmpty ? points : scaledPoints
    }

    private func appendScaledSegment(
        _ segment: [Measurements.Measurement.Point],
        to scaledPoints: inout [Measurements.Measurement.Point],
        displayColumns: Int
    ) {
        guard !segment.isEmpty else { return }

        if segment.count <= max(displayColumns * 2, 120) {
            for point in segment {
                appendScaledPoint(point, to: &scaledPoints)
            }
            return
        }

        var bucket: [Measurements.Measurement.Point] = []
        var currentColumn: Int?

        for point in segment {
            let column = displayColumn(for: point, totalColumns: displayColumns)

            if let currentColumn, currentColumn != column {
                appendBucket(bucket, to: &scaledPoints)
                bucket.removeAll(keepingCapacity: true)
            }

            bucket.append(point)
            currentColumn = column
        }

        appendBucket(bucket, to: &scaledPoints)
    }

    private func appendBucket(
        _ bucket: [Measurements.Measurement.Point],
        to scaledPoints: inout [Measurements.Measurement.Point]
    ) {
        guard !bucket.isEmpty else { return }

        if bucket.count <= 2 {
            for point in bucket {
                appendScaledPoint(point, to: &scaledPoints)
            }
            return
        }

        let firstPoint = bucket.first!
        let lastPoint = bucket.last!
        let minimumPoint = bucket.min { lhs, rhs in lhs.value < rhs.value } ?? firstPoint
        let maximumPoint = bucket.max { lhs, rhs in lhs.value < rhs.value } ?? firstPoint

        let orderedPoints = [firstPoint, minimumPoint, maximumPoint, lastPoint]
            .sorted { lhs, rhs in
                if lhs.timestamp == rhs.timestamp {
                    return lhs.id < rhs.id
                }
                return lhs.timestamp < rhs.timestamp
            }

        var emittedPointIDs: Set<Int> = []
        for point in orderedPoints where emittedPointIDs.insert(point.id).inserted {
            appendScaledPoint(point, to: &scaledPoints)
        }
    }

    private func appendScaledPoint(
        _ point: Measurements.Measurement.Point,
        to scaledPoints: inout [Measurements.Measurement.Point]
    ) {
        guard !(scaledPoints.last?.timestamp == point.timestamp &&
                scaledPoints.last?.value == point.value &&
                scaledPoints.last?.kind == point.kind) else {
            return
        }

        scaledPoints.append(
            Measurements.Measurement.Point(
                id: scaledPoints.count,
                timestamp: point.timestamp,
                value: point.value,
                kind: point.kind
            )
        )
    }

    private func displayColumn(
        for point: Measurements.Measurement.Point,
        totalColumns: Int
    ) -> Int {
        let totalColumns = max(totalColumns, 1)
        let timeSpan = max(Double(context.size.width), 1)
        let normalizedOffset = min(
            max((point.timestamp.timeIntervalSince1970 - Double(context.origin.x)) / timeSpan, 0),
            1
        )

        return min(
            Int((normalizedOffset * Double(totalColumns - 1)).rounded(.down)),
            totalColumns - 1
        )
    }
    
}