Charts: puntos + labels de valor en todas las gráficas de líneas
- Nuevo ChartValueLabels.swift: ChartValueBubble (cápsula) y ChartSelectionCard
(tarjeta multi-serie), con umbral compartido (<=12 puntos → labels SIEMPRE
visibles; las gráficas cortas aportan mucho más con los valores a la vista)
- PointMark añadido donde faltaba (Compare, Year vs Year, Drawdown, Volatility);
agrandado donde ya existía (Evolution, Prediction, Period vs Period, Rolling)
- Selección por tap/arrastre (chartXSelection) en las 8 gráficas: RuleMark + burbuja
prominente (una serie) o tarjeta con el valor de cada serie (multi-serie)
- Evolution reutiliza su scrubbing existente y ahora muestra burbuja en el punto
- Period vs Period: labels A arriba / B abajo para evitar solapes
- Drawdown/Volatility migrados de Chart(data:) a Chart{ForEach} para poder añadir
marcas de selección
Verificado en iPad Pro 13" (labels visibles en Evolution y Period vs Period).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
This commit is contained in:
@@ -4,6 +4,15 @@ import Charts
|
||||
struct DrawdownChart: View {
|
||||
let data: [(date: Date, drawdown: Double)]
|
||||
@State private var zoom = ChartZoomModel()
|
||||
@State private var selectedDate: Date?
|
||||
|
||||
private var showAllLabels: Bool { data.count <= ChartLabels.alwaysShowThreshold }
|
||||
private var selectedPoint: (date: Date, drawdown: Double)? {
|
||||
guard let selectedDate else { return nil }
|
||||
return data.min(by: {
|
||||
abs($0.date.timeIntervalSince(selectedDate)) < abs($1.date.timeIntervalSince(selectedDate))
|
||||
})
|
||||
}
|
||||
|
||||
var maxDrawdown: Double {
|
||||
abs(data.map { $0.drawdown }.min() ?? 0)
|
||||
@@ -36,27 +45,57 @@ struct DrawdownChart: View {
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if data.count >= 2 {
|
||||
Chart(data, id: \.date) { item in
|
||||
AreaMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Drawdown", item.drawdown)
|
||||
)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [Color.negativeRed.opacity(0.5), Color.negativeRed.opacity(0.1)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
Chart {
|
||||
ForEach(data, id: \.date) { item in
|
||||
AreaMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Drawdown", item.drawdown)
|
||||
)
|
||||
)
|
||||
.interpolationMethod(.catmullRom)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [Color.negativeRed.opacity(0.5), Color.negativeRed.opacity(0.1)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.interpolationMethod(.catmullRom)
|
||||
|
||||
LineMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Drawdown", item.drawdown)
|
||||
)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
.interpolationMethod(.catmullRom)
|
||||
LineMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Drawdown", item.drawdown)
|
||||
)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
.interpolationMethod(.catmullRom)
|
||||
|
||||
PointMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Drawdown", item.drawdown)
|
||||
)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
.symbolSize(showAllLabels ? 36 : 20)
|
||||
.annotation(position: .bottom, spacing: 3) {
|
||||
if showAllLabels {
|
||||
ChartValueBubble(text: ChartLabels.percent(item.drawdown), color: .negativeRed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let sel = selectedPoint {
|
||||
RuleMark(x: .value("Selected", sel.date))
|
||||
.foregroundStyle(Color.secondary.opacity(0.35))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
|
||||
PointMark(
|
||||
x: .value("Selected", sel.date),
|
||||
y: .value("Drawdown", sel.drawdown)
|
||||
)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
.symbolSize(70)
|
||||
.annotation(position: .bottom, spacing: 4) {
|
||||
ChartValueBubble(text: ChartLabels.percent(sel.drawdown), color: .negativeRed, prominent: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.chartXSelection(value: $selectedDate)
|
||||
.chartXAxis {
|
||||
AxisMarks(values: .stride(by: .month, count: 2)) { value in
|
||||
AxisValueLabel(format: .dateTime.month(.abbreviated))
|
||||
@@ -167,6 +206,15 @@ struct DrawdownStatView: View {
|
||||
struct VolatilityChartView: View {
|
||||
let data: [(date: Date, volatility: Double)]
|
||||
@State private var zoom = ChartZoomModel()
|
||||
@State private var selectedDate: Date?
|
||||
|
||||
private var showAllLabels: Bool { data.count <= ChartLabels.alwaysShowThreshold }
|
||||
private var selectedPoint: (date: Date, volatility: Double)? {
|
||||
guard let selectedDate else { return nil }
|
||||
return data.min(by: {
|
||||
abs($0.date.timeIntervalSince(selectedDate)) < abs($1.date.timeIntervalSince(selectedDate))
|
||||
})
|
||||
}
|
||||
|
||||
var currentVolatility: Double {
|
||||
data.last?.volatility ?? 0
|
||||
@@ -191,26 +239,40 @@ struct VolatilityChartView: View {
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if data.count >= 2 {
|
||||
Chart(data, id: \.date) { item in
|
||||
LineMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Volatility", item.volatility)
|
||||
)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.interpolationMethod(.catmullRom)
|
||||
|
||||
AreaMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Volatility", item.volatility)
|
||||
)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [Color.appPrimary.opacity(0.3), Color.appPrimary.opacity(0.0)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
Chart {
|
||||
ForEach(data, id: \.date) { item in
|
||||
LineMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Volatility", item.volatility)
|
||||
)
|
||||
)
|
||||
.interpolationMethod(.catmullRom)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.interpolationMethod(.catmullRom)
|
||||
|
||||
AreaMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Volatility", item.volatility)
|
||||
)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [Color.appPrimary.opacity(0.3), Color.appPrimary.opacity(0.0)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.interpolationMethod(.catmullRom)
|
||||
|
||||
PointMark(
|
||||
x: .value("Date", item.date),
|
||||
y: .value("Volatility", item.volatility)
|
||||
)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.symbolSize(showAllLabels ? 36 : 20)
|
||||
.annotation(position: .top, spacing: 3) {
|
||||
if showAllLabels {
|
||||
ChartValueBubble(text: String(format: "%.1f%%", item.volatility))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Average line
|
||||
RuleMark(y: .value("Average", averageVolatility))
|
||||
@@ -221,7 +283,23 @@ struct VolatilityChartView: View {
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if let sel = selectedPoint {
|
||||
RuleMark(x: .value("Selected", sel.date))
|
||||
.foregroundStyle(Color.secondary.opacity(0.35))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
|
||||
PointMark(
|
||||
x: .value("Selected", sel.date),
|
||||
y: .value("Volatility", sel.volatility)
|
||||
)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.symbolSize(70)
|
||||
.annotation(position: .top, spacing: 4) {
|
||||
ChartValueBubble(text: String(format: "%.1f%%", sel.volatility), prominent: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.chartXSelection(value: $selectedDate)
|
||||
.chartXAxis {
|
||||
AxisMarks(values: .stride(by: .month, count: 2)) { value in
|
||||
AxisValueLabel(format: .dateTime.month(.abbreviated))
|
||||
|
||||
Reference in New Issue
Block a user