Phase 1 - Design System: DesignSystem.swift (typography, colors, spacing constants) and DataPanel.swift (reusable panel container with 3 densities and optional team accent bar). Phase 2 - Dashboard Density: LiveSituationBar (compact strip of all live games with scores/innings/outs), MiniLinescoreView (R-H-E footer for game cards), DiamondView (visual baseball diamond with runners and count). Dashboard shows live situation bar when games are active. Game cards now display mini linescore for live/final games. Phase 3 - Game Center Intelligence: WinProbabilityChartView (full-game line chart using Swift Charts with area fills), PitchArsenalView (pitch type distribution with velocity bars). GameCenterViewModel now stores full WP history array instead of just latest values. Phase 4 - Feed Tab: MLBWebDataService (fetches league leaders from Stats API, news headlines, transactions), FeedViewModel, FeedView with reverse-chronological feed items. FeedItemView with colored edge bars by category. Added 5th "Feed" tab to both tvOS and iOS. Phase 5 - Intel Tab: LeaderboardView (top-5 stat cards with headshots), integrated into LeagueCenterView. Renamed tabs: Games->Today, League->Intel. LeagueCenterViewModel now fetches league leaders. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
95 lines
3.5 KiB
Swift
95 lines
3.5 KiB
Swift
import Charts
|
|
import SwiftUI
|
|
|
|
/// Full-game win probability line chart using Swift Charts
|
|
struct WinProbabilityChartView: View {
|
|
let entries: [WinProbabilityEntry]
|
|
let homeCode: String
|
|
let awayCode: String
|
|
|
|
private var homeColor: Color { TeamAssets.color(for: homeCode) }
|
|
private var awayColor: Color { TeamAssets.color(for: awayCode) }
|
|
|
|
var body: some View {
|
|
DataPanel(.standard) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("WIN PROBABILITY")
|
|
.dataLabelStyle()
|
|
Spacer()
|
|
if let latest = entries.last {
|
|
HStack(spacing: 12) {
|
|
probLabel(code: awayCode, value: latest.awayTeamWinProbability)
|
|
probLabel(code: homeCode, value: latest.homeTeamWinProbability)
|
|
}
|
|
}
|
|
}
|
|
|
|
Chart {
|
|
ForEach(Array(entries.enumerated()), id: \.offset) { index, entry in
|
|
if let wp = entry.homeTeamWinProbability {
|
|
LineMark(
|
|
x: .value("AB", index),
|
|
y: .value("WP", wp)
|
|
)
|
|
.foregroundStyle(homeColor)
|
|
.interpolationMethod(.catmullRom)
|
|
|
|
AreaMark(
|
|
x: .value("AB", index),
|
|
yStart: .value("Base", 50),
|
|
yEnd: .value("WP", wp)
|
|
)
|
|
.foregroundStyle(
|
|
wp >= 50
|
|
? homeColor.opacity(0.15)
|
|
: awayColor.opacity(0.15)
|
|
)
|
|
.interpolationMethod(.catmullRom)
|
|
}
|
|
}
|
|
|
|
RuleMark(y: .value("Even", 50))
|
|
.foregroundStyle(DS.Colors.textQuaternary)
|
|
.lineStyle(StrokeStyle(lineWidth: 0.5, dash: [4, 4]))
|
|
}
|
|
.chartYScale(domain: 0...100)
|
|
.chartYAxis {
|
|
AxisMarks(values: [0, 25, 50, 75, 100]) { value in
|
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.3))
|
|
.foregroundStyle(DS.Colors.textQuaternary)
|
|
AxisValueLabel {
|
|
Text("\(value.as(Int.self) ?? 0)%")
|
|
.font(DS.Fonts.caption)
|
|
.foregroundStyle(DS.Colors.textTertiary)
|
|
}
|
|
}
|
|
}
|
|
.chartXAxis(.hidden)
|
|
.frame(height: chartHeight)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func probLabel(code: String, value: Double?) -> some View {
|
|
HStack(spacing: 4) {
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(TeamAssets.color(for: code))
|
|
.frame(width: 3, height: 14)
|
|
Text(code)
|
|
.font(DS.Fonts.caption)
|
|
.foregroundStyle(DS.Colors.textTertiary)
|
|
Text(value.map { "\(Int($0))%" } ?? "-")
|
|
.font(DS.Fonts.dataValueCompact)
|
|
.foregroundStyle(DS.Colors.textPrimary)
|
|
}
|
|
}
|
|
|
|
#if os(tvOS)
|
|
private var chartHeight: CGFloat { 180 }
|
|
#else
|
|
private var chartHeight: CGFloat { 140 }
|
|
#endif
|
|
}
|