-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathmain.go
More file actions
185 lines (167 loc) · 5.09 KB
/
Copy pathmain.go
File metadata and controls
185 lines (167 loc) · 5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// examples/chartpicture/main.go
//
// Single-pane demo of chartpicture.Model:
// - Live-updating line chart, new data point every 500ms (60-point window).
// - 't' cycles the go-analyze/charts theme.
// - 'g' toggles Glyph / Kitty rendering.
// - 'r' swaps between line-chart and bar-chart sample datasets.
// - 'q' or ctrl+c quits.
package main
import (
"fmt"
"math"
"math/rand/v2"
"os"
"time"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
booba "github.com/NimbleMarkets/go-booba"
"github.com/NimbleMarkets/ntcharts/v2/picture"
"github.com/NimbleMarkets/ntcharts/v2/picture/chartpicture"
"github.com/go-analyze/charts"
)
const (
kittyID = 4244
tickPeriod = 500 * time.Millisecond
windowLen = 60
)
var themes = []string{"light", "dark", "vivid-light", "vivid-dark"}
type tickMsg time.Time
func tickCmd() tea.Cmd {
return tea.Tick(tickPeriod, func(t time.Time) tea.Msg { return tickMsg(t) })
}
type model struct {
chart chartpicture.Model
width, height int
themeIdx int
mode string // "line" or "bar"
series []float64
initCmd tea.Cmd
}
func initialModel() model {
c := chartpicture.NewWithConfig(chartpicture.Config{
KittyID: kittyID,
Theme: themes[0],
})
m := model{chart: c, mode: "line"}
m.series = generateSeries(windowLen)
cmd := m.chart.SetLineChartOption(lineOptFromSeries(m.series))
m.initCmd = tea.Batch(m.chart.Init(), cmd, tickCmd())
return m
}
func generateSeries(n int) []float64 {
out := make([]float64, n)
for i := range n {
out[i] = 50 + 20*math.Sin(float64(i)/4) + rand.Float64()*5
}
return out
}
func lineOptFromSeries(values []float64) charts.LineChartOption {
opt := charts.NewLineChartOptionWithData([][]float64{values})
opt.Title = charts.TitleOption{Text: "live data"}
return opt
}
func barOpt() charts.BarChartOption {
opt := charts.NewBarChartOptionWithData([][]float64{{12, 24, 18, 30, 22}})
opt.Title = charts.TitleOption{Text: "quarters"}
opt.CategoryAxis = charts.CategoryAxisOption{Labels: []string{"Q1", "Q2", "Q3", "Q4", "Q5"}}
return opt
}
func (m model) Init() tea.Cmd { return m.initCmd }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "g":
if c := m.chart.Toggle(); c != nil {
cmds = append(cmds, c)
}
case "t":
m.themeIdx = (m.themeIdx + 1) % len(themes)
if c := m.chart.SetTheme(themes[m.themeIdx]); c != nil {
cmds = append(cmds, c)
}
case "r":
if m.mode == "line" {
m.mode = "bar"
if c := m.chart.SetBarChartOption(barOpt()); c != nil {
cmds = append(cmds, c)
}
} else {
m.mode = "line"
if c := m.chart.SetLineChartOption(lineOptFromSeries(m.series)); c != nil {
cmds = append(cmds, c)
}
}
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
// Pane is m.height-3 outer with a 1-cell border, so content area is
// (m.width-2) × (m.height-5). Match the chart to that exactly so
// Kitty's placeholder grid isn't truncated by lipgloss (which would
// squash the placement vertically into one fewer row, making Kitty
// and Glyph fill the pane inconsistently).
if c := m.chart.SetSize(m.width-2, m.height-5); c != nil {
cmds = append(cmds, c)
}
case tickMsg:
// Append a new sample, drop the oldest.
next := 50 + 20*math.Sin(float64(time.Now().UnixNano()/int64(tickPeriod))/4) + rand.Float64()*5
m.series = append(m.series[1:], next)
if m.mode == "line" {
if c := m.chart.SetLineChartOption(lineOptFromSeries(m.series)); c != nil {
cmds = append(cmds, c)
}
}
cmds = append(cmds, tickCmd())
}
if c := m.chart.Update(msg); c != nil {
cmds = append(cmds, c)
}
return m, tea.Batch(cmds...)
}
func (m model) View() tea.View {
title := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("12")).
Width(m.width).
Align(lipgloss.Center).
Render("📈 ntcharts · chartpicture")
pane := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("8")).
Width(m.width).
Height(m.height-3).
Align(lipgloss.Center, lipgloss.Center).
Render(m.chart.View().Content)
mode := "Glyph"
if m.chart.Mode() == picture.PictureKitty {
mode = "Kitty"
}
footer := lipgloss.NewStyle().
Width(m.width).
Foreground(lipgloss.Color("242")).
Render(fmt.Sprintf("theme: %s · render: %s · type: %s · t theme · g toggle · r switch · q quit",
themes[m.themeIdx], mode, m.mode))
parts := []string{title, pane, footer}
if err := m.chart.Err(); err != nil {
errBar := lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render(err.Error())
parts = []string{title, pane, errBar, footer}
}
v := tea.NewView(lipgloss.JoinVertical(lipgloss.Left, parts...))
v.AltScreen = true
return v
}
func main() {
// booba.Run is a tea.Program substitute that dispatches to native Bubble Tea
// or the WASM/ghostty-web bridge depending on build target.
// See https://github.com/NimbleMarkets/go-booba-example.
if err := booba.Run(initialModel()); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}