mirror of
https://github.com/sstent/aicyclingcoach-go.git
synced 2026-01-26 09:02:18 +00:00
166 lines
3.8 KiB
Go
166 lines
3.8 KiB
Go
package components
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/sstent/aicyclingcoach-go/fitness-tui/internal/types"
|
|
)
|
|
|
|
// Chart represents an ASCII chart component
|
|
type Chart struct {
|
|
Data []float64
|
|
Title string
|
|
Width int
|
|
Height int
|
|
Color lipgloss.Color
|
|
downsampler *types.Downsampler
|
|
}
|
|
|
|
// NewChart creates a new Chart instance
|
|
func NewChart(data []float64, title string) *Chart {
|
|
return &Chart{
|
|
Data: data,
|
|
Title: title,
|
|
Width: 0, // Will be set based on terminal size
|
|
Height: 10,
|
|
Color: lipgloss.Color("39"), // Default blue
|
|
downsampler: types.NewDownsampler(),
|
|
}
|
|
}
|
|
|
|
// WithSize sets the chart dimensions
|
|
func (c *Chart) WithSize(width, height int) *Chart {
|
|
c.Width = width
|
|
c.Height = height
|
|
return c
|
|
}
|
|
|
|
// WithColor sets the chart color
|
|
func (c *Chart) WithColor(color lipgloss.Color) *Chart {
|
|
c.Color = color
|
|
return c
|
|
}
|
|
|
|
// View renders the chart
|
|
func (c *Chart) View() string {
|
|
if len(c.Data) == 0 {
|
|
return fmt.Sprintf("%s\nNo data available", c.Title)
|
|
}
|
|
|
|
// Downsample data if needed
|
|
processedData := c.downsampler.Process(c.Data, c.Width)
|
|
|
|
// Normalize data to chart height
|
|
min, max := minMax(processedData)
|
|
normalized := normalize(processedData, min, max, c.Height-1)
|
|
|
|
// Build chart
|
|
var sb strings.Builder
|
|
sb.WriteString(c.Title + "\n")
|
|
|
|
// Create Y-axis labels
|
|
yLabels := createYAxisLabels(min, max, c.Height-1)
|
|
|
|
for i := c.Height - 1; i >= 0; i-- {
|
|
if i == 0 {
|
|
sb.WriteString("└") // Bottom-left corner
|
|
} else if i == c.Height-1 {
|
|
sb.WriteString("↑") // Top axis indicator
|
|
} else {
|
|
sb.WriteString("│") // Y-axis line
|
|
}
|
|
|
|
// Add Y-axis label
|
|
if i < len(yLabels) {
|
|
sb.WriteString(yLabels[i])
|
|
} else {
|
|
sb.WriteString(" ")
|
|
}
|
|
|
|
// Add chart bars
|
|
for j := 0; j < len(normalized); j++ {
|
|
if i == 0 {
|
|
sb.WriteString("─") // X-axis
|
|
} else {
|
|
if normalized[j] >= float64(i) {
|
|
sb.WriteString("█") // Full block
|
|
} else {
|
|
// Gradient blocks based on fractional part
|
|
frac := normalized[j] - math.Floor(normalized[j])
|
|
if normalized[j] >= float64(i-1) && frac > 0.75 {
|
|
sb.WriteString("▇")
|
|
} else if normalized[j] >= float64(i-1) && frac > 0.5 {
|
|
sb.WriteString("▅")
|
|
} else if normalized[j] >= float64(i-1) && frac > 0.25 {
|
|
sb.WriteString("▃")
|
|
} else if normalized[j] >= float64(i-1) && frac > 0 {
|
|
sb.WriteString("▁")
|
|
} else {
|
|
sb.WriteString(" ")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Add X-axis title
|
|
sb.WriteString(" " + strings.Repeat(" ", len(yLabels[0])+1) + "→ Time\n")
|
|
|
|
// Apply color styling
|
|
style := lipgloss.NewStyle().Foreground(c.Color)
|
|
return style.Render(sb.String())
|
|
}
|
|
|
|
// minMax finds min and max values in a slice
|
|
func minMax(data []float64) (min, max float64) {
|
|
if len(data) == 0 {
|
|
return 0, 0
|
|
}
|
|
min = data[0]
|
|
max = data[0]
|
|
for _, v := range data {
|
|
if v < min {
|
|
min = v
|
|
}
|
|
if v > max {
|
|
max = v
|
|
}
|
|
}
|
|
return min, max
|
|
}
|
|
|
|
// normalize scales values to fit within chart height
|
|
func normalize(data []float64, min, max float64, height int) []float64 {
|
|
if max == min || height <= 0 {
|
|
return make([]float64, len(data))
|
|
}
|
|
|
|
scale := float64(height) / (max - min)
|
|
normalized := make([]float64, len(data))
|
|
for i, v := range data {
|
|
normalized[i] = (v - min) * scale
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
// createYAxisLabels creates labels for Y-axis
|
|
func createYAxisLabels(min, max float64, height int) []string {
|
|
labels := make([]string, height+1)
|
|
step := (max - min) / float64(height)
|
|
|
|
for i := 0; i <= height; i++ {
|
|
value := min + float64(i)*step
|
|
label := fmt.Sprintf("%.0f", value)
|
|
// Pad to consistent width (5 characters)
|
|
if len(label) < 5 {
|
|
label = strings.Repeat(" ", 5-len(label)) + label
|
|
}
|
|
labels[height-i] = label
|
|
}
|
|
return labels
|
|
}
|