SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
2.8 KB · 74 lines swift
Raw Blame History
1//2//  PlotView.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Charts11import SwiftUI12import ZQGraphics1314/// Swift Charts rendering of the session's latest plot spec. The custom15/// Metal renderer takes over past ~1M points (v0.3, CLAUDE.md §7).16struct PlotView: View {17    @Environment(SessionModel.self) private var model1819    /// Past this many points, Swift Charts degrades — the Metal renderer20    /// takes over (overridable for tests via METRIKA_METAL_THRESHOLD).21    private static let metalThreshold: Int = {22        if let text = ProcessInfo.processInfo.environment["METRIKA_METAL_THRESHOLD"],23           let value = Int(text) {24            return value25        }26        return 100_00027    }()2829    var body: some View {30        if let plot = model.lastPlot,31           plot.kind == .scatter,32           plot.series.reduce(0, { $0 + $1.x.count }) > Self.metalThreshold {33            MetalScatterPane(plot: plot)34                .accessibilityLabel("Metal scatter renderer")35        } else if let plot = model.lastPlot {36            Chart {37                ForEach(Array(plot.series.enumerated()), id: \.offset) { _, series in38                    ForEach(series.x.indices, id: \.self) { index in39                        switch plot.kind {40                        case .scatter:41                            PointMark(42                                x: .value(plot.xLabel, series.x[index]),43                                y: .value(plot.yLabel, series.y[index])44                            )45                            .symbolSize(20)46                            .foregroundStyle(by: .value("Series", series.label))47                        case .line, .kdensity:48                            LineMark(49                                x: .value(plot.xLabel, series.x[index]),50                                y: .value(plot.yLabel, series.y[index])51                            )52                            .foregroundStyle(by: .value("Series", series.label))53                        case .histogram:54                            BarMark(55                                x: .value(plot.xLabel, series.x[index]),56                                y: .value(plot.yLabel, series.y[index])57                            )58                            .foregroundStyle(by: .value("Series", series.label))59                        }60                    }61                }62            }63            .chartXAxisLabel(plot.xLabel)64            .chartYAxisLabel(plot.yLabel)65            .padding(12)66        } else {67            ContentUnavailableView(68                "No plot", systemImage: "chart.xyaxis.line",69                description: Text("Run a graph command to see it here.")70            )71        }72    }73}74