// // PlotView.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import Charts import SwiftUI import ZQGraphics /// Swift Charts rendering of the session's latest plot spec. The custom /// Metal renderer takes over past ~1M points (v0.3, CLAUDE.md §7). struct PlotView: View { @Environment(SessionModel.self) private var model /// Past this many points, Swift Charts degrades — the Metal renderer /// takes over (overridable for tests via METRIKA_METAL_THRESHOLD). private static let metalThreshold: Int = { if let text = ProcessInfo.processInfo.environment["METRIKA_METAL_THRESHOLD"], let value = Int(text) { return value } return 100_000 }() var body: some View { if let plot = model.lastPlot, plot.kind == .scatter, plot.series.reduce(0, { $0 + $1.x.count }) > Self.metalThreshold { MetalScatterPane(plot: plot) .accessibilityLabel("Metal scatter renderer") } else if let plot = model.lastPlot { Chart { ForEach(Array(plot.series.enumerated()), id: \.offset) { _, series in ForEach(series.x.indices, id: \.self) { index in switch plot.kind { case .scatter: PointMark( x: .value(plot.xLabel, series.x[index]), y: .value(plot.yLabel, series.y[index]) ) .symbolSize(20) .foregroundStyle(by: .value("Series", series.label)) case .line, .kdensity: LineMark( x: .value(plot.xLabel, series.x[index]), y: .value(plot.yLabel, series.y[index]) ) .foregroundStyle(by: .value("Series", series.label)) case .histogram: BarMark( x: .value(plot.xLabel, series.x[index]), y: .value(plot.yLabel, series.y[index]) ) .foregroundStyle(by: .value("Series", series.label)) } } } } .chartXAxisLabel(plot.xLabel) .chartYAxisLabel(plot.yLabel) .padding(12) } else { ContentUnavailableView( "No plot", systemImage: "chart.xyaxis.line", description: Text("Run a graph command to see it here.") ) } } }