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
@@ -999,7 +999,12 @@ struct EvolutionChartView: View {
y: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue)
)
.foregroundStyle(Color.appPrimary)
.symbolSize(30)
.symbolSize(data.count <= ChartLabels.alwaysShowThreshold ? 40 : 30)
.annotation(position: .top, spacing: 3) {
if data.count <= ChartLabels.alwaysShowThreshold {
ChartValueBubble(text: item.value.compactCurrencyString)
}
}
AreaMark(
x: .value("Date", item.date),
@@ -1049,6 +1054,22 @@ struct EvolutionChartView: View {
.lineStyle(StrokeStyle(lineWidth: 1, dash: [6, 4]))
}
}
// Scrub/tap selection bubble (total mode)
if chartMode == .total, let sel = selectedDataPoint {
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("Value", NSDecimalNumber(decimal: sel.value).doubleValue)
)
.foregroundStyle(Color.appPrimary)
.symbolSize(80)
.annotation(position: .top, spacing: 4) {
ChartValueBubble(text: sel.value.compactCurrencyString, prominent: true)
}
}
}
private var chartCategoryNames: [String] {
@@ -1139,6 +1160,15 @@ struct ContributionsChartView: View {
struct RollingReturnChartView: View {
let data: [(date: Date, value: Double)]
@State private var zoom = ChartZoomModel()
@State private var selectedDate: Date?
private var showAllLabels: Bool { data.count <= ChartLabels.alwaysShowThreshold }
private var selectedPoint: (date: Date, value: Double)? {
guard let selectedDate else { return nil }
return data.min(by: {
abs($0.date.timeIntervalSince(selectedDate)) < abs($1.date.timeIntervalSince(selectedDate))
})
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -1164,9 +1194,30 @@ struct RollingReturnChartView: View {
y: .value("Return", item.value)
)
.foregroundStyle(Color.appPrimary)
.symbolSize(24)
.symbolSize(showAllLabels ? 36 : 24)
.annotation(position: item.value >= 0 ? .top : .bottom, spacing: 3) {
if showAllLabels {
ChartValueBubble(text: ChartLabels.percent(item.value))
}
}
}
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("Return", sel.value)
)
.foregroundStyle(Color.appPrimary)
.symbolSize(70)
.annotation(position: sel.value >= 0 ? .top : .bottom, spacing: 4) {
ChartValueBubble(text: ChartLabels.percent(sel.value), prominent: true)
}
}
}
.chartXSelection(value: $selectedDate)
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 2)) { value in
AxisValueLabel(format: .dateTime.month(.abbreviated))