mirror of
https://github.com/sstent/aicyclingcoach-go.git
synced 2025-12-06 08:01:38 +00:00
go baby go
This commit is contained in:
92
fitness-tui/internal/tui/components/chart.go
Normal file
92
fitness-tui/internal/tui/components/chart.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const (
|
||||
BlockEmpty = " "
|
||||
Block1 = "▁"
|
||||
Block2 = "▂"
|
||||
Block3 = "▃"
|
||||
Block4 = "▄"
|
||||
Block5 = "▅"
|
||||
Block6 = "▆"
|
||||
Block7 = "▇"
|
||||
Block8 = "█"
|
||||
)
|
||||
|
||||
var blockChars = []string{BlockEmpty, Block1, Block2, Block3, Block4, Block5, Block6, Block7, Block8}
|
||||
|
||||
type Chart struct {
|
||||
Width int
|
||||
Height int
|
||||
Data []float64
|
||||
Title string
|
||||
style lipgloss.Style
|
||||
}
|
||||
|
||||
func NewChart(data []float64, width, height int, title string) *Chart {
|
||||
return &Chart{
|
||||
Data: data,
|
||||
Width: width,
|
||||
Height: height,
|
||||
Title: title,
|
||||
style: lipgloss.NewStyle().Padding(0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chart) View() string {
|
||||
if len(c.Data) == 0 {
|
||||
return c.style.Render("No data available")
|
||||
}
|
||||
|
||||
min, max := findMinMax(c.Data)
|
||||
sampled := sampleData(c.Data, c.Width)
|
||||
|
||||
var chart strings.Builder
|
||||
for _, value := range sampled {
|
||||
normalized := (value - min) / (max - min)
|
||||
level := int(normalized * 8)
|
||||
if level > 8 {
|
||||
level = 8
|
||||
}
|
||||
chart.WriteString(blockChars[level])
|
||||
}
|
||||
|
||||
return c.style.Render(fmt.Sprintf("%s\n%s", c.Title, chart.String()))
|
||||
}
|
||||
|
||||
func findMinMax(data []float64) (float64, float64) {
|
||||
min, max := data[0], data[0]
|
||||
for _, v := range data {
|
||||
if v < min {
|
||||
min = v
|
||||
}
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return min, max
|
||||
}
|
||||
|
||||
func sampleData(data []float64, targetLength int) []float64 {
|
||||
if len(data) <= targetLength {
|
||||
return data
|
||||
}
|
||||
|
||||
sampled := make([]float64, targetLength)
|
||||
ratio := float64(len(data)) / float64(targetLength)
|
||||
|
||||
for i := 0; i < targetLength; i++ {
|
||||
index := int(float64(i) * ratio)
|
||||
if index >= len(data) {
|
||||
index = len(data) - 1
|
||||
}
|
||||
sampled[i] = data[index]
|
||||
}
|
||||
return sampled
|
||||
}
|
||||
38
fitness-tui/internal/tui/components/chart_test.go
Normal file
38
fitness-tui/internal/tui/components/chart_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChartView(t *testing.T) {
|
||||
t.Run("empty data", func(t *testing.T) {
|
||||
chart := NewChart(nil, 10, 4, "Test")
|
||||
view := chart.View()
|
||||
assert.Contains(t, view, "No data available")
|
||||
})
|
||||
|
||||
t.Run("single data point", func(t *testing.T) {
|
||||
chart := NewChart([]float64{50}, 5, 4, "Single")
|
||||
view := chart.View()
|
||||
assert.Equal(t, "Single\n▄▄▄▄▄", view)
|
||||
})
|
||||
|
||||
t.Run("multiple data points", func(t *testing.T) {
|
||||
data := []float64{10, 20, 30, 40, 50}
|
||||
chart := NewChart(data, 5, 4, "Series")
|
||||
view := chart.View()
|
||||
assert.Equal(t, "Series\n▁▂▄▆█", view)
|
||||
})
|
||||
|
||||
t.Run("downsampling", func(t *testing.T) {
|
||||
data := make([]float64, 100)
|
||||
for i := range data {
|
||||
data[i] = float64(i)
|
||||
}
|
||||
chart := NewChart(data, 20, 4, "Downsample")
|
||||
view := chart.View()
|
||||
assert.Len(t, view, 20+6) // Title + chart characters
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user