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:
alexandrev-tibco
2026-07-03 23:09:35 +02:00
parent 7c92ea36c0
commit f7d4fdbe2d
7 changed files with 409 additions and 42 deletions
@@ -2,6 +2,14 @@ import SwiftUI
import Charts
struct ComparisonChartView: View {
@State private var selectedDate: Date?
private static let selectionDateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMM yyyy"
return f
}()
@ObservedObject var viewModel: ChartsViewModel
@@ -102,6 +110,21 @@ struct ComparisonChartView: View {
.frame(height: 260)
}
/// Permanent labels only when the chart stays readable (few points AND few series).
private var showAllLabels: Bool {
let maxPoints = viewModel.comparisonData.map { $0.points.count }.max() ?? 0
return maxPoints <= ChartLabels.alwaysShowThreshold && viewModel.comparisonData.count <= 3
}
private func selectionRows(for date: Date) -> [(label: String, value: String, color: Color)] {
viewModel.comparisonData.compactMap { series in
guard let point = series.points.min(by: {
abs($0.date.timeIntervalSince(date)) < abs($1.date.timeIntervalSince(date))
}) else { return nil }
return (series.name, yAxisLabel(for: point.value), Color(hex: series.colorHex) ?? .appPrimary)
}
}
private var lineChart: some View {
VStack(spacing: 12) {
Chart {
@@ -115,9 +138,34 @@ struct ComparisonChartView: View {
)
.foregroundStyle(seriesColor)
.interpolationMethod(.catmullRom)
PointMark(
x: .value("Date", point.date),
y: .value("Value", point.value)
)
.foregroundStyle(seriesColor)
.symbolSize(showAllLabels ? 32 : 16)
.annotation(position: .top, spacing: 2) {
if showAllLabels {
ChartValueBubble(text: yAxisLabel(for: point.value), color: seriesColor)
}
}
}
}
if let selectedDate {
RuleMark(x: .value("Selected", selectedDate))
.foregroundStyle(Color.secondary.opacity(0.35))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
.annotation(position: .top, spacing: 4) {
ChartSelectionCard(
title: Self.selectionDateFormatter.string(from: selectedDate),
rows: selectionRows(for: selectedDate)
)
}
}
}
.chartXSelection(value: $selectedDate)
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: xAxisStride)) { value in
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [3, 3]))