timechart.go 12.3 KB
Newer Older
liming6's avatar
liming6 committed
1
2
3
4
5
6
package tui

import (
	"sort"
	"strconv"
	"strings"
liming6's avatar
liming6 committed
7
	"sync"
liming6's avatar
liming6 committed
8
9
	"time"

10
	"get-container/cmd/hytop/tchart"
liming6's avatar
liming6 committed
11
	"get-container/utils"
liming6's avatar
liming6 committed
12

liming6's avatar
liming6 committed
13
14
15
	"github.com/NimbleMarkets/ntcharts/canvas/runes"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
liming6's avatar
liming6 committed
16
	linkedlist "github.com/emirpasic/gods/v2/lists/doublylinkedlist"
liming6's avatar
liming6 committed
17
18
19
20
21
	zone "github.com/lrstanley/bubblezone"
)

const (
	A = "├"
liming6's avatar
liming6 committed
22
	B = "└"
liming6's avatar
liming6 committed
23
24
25
)

var (
26
	axisFStyle = lipgloss.NewStyle().Inline(true).Foreground(lipgloss.Color("#4d4d4dff"))
liming6's avatar
liming6 committed
27
28
29
)

// genXAxis 生成X轴,参数l是x轴的长度
liming6's avatar
liming6 committed
30
31
32
33
34
func genXAxis(l int, s ...string) string {
	if s == nil {
		s = make([]string, 0, 1)
		s = append(s, A)
	}
liming6's avatar
liming6 committed
35
36
37
38
	t60 := l / 30
	t30 := l >= 18
	var result string
	if t30 {
liming6's avatar
liming6 committed
39
		result = s[0] + strings.Repeat("─", 14)
liming6's avatar
liming6 committed
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
		result = axisFStyle.Render("30s") + result
	} else {
		return strings.Repeat("─", l)
	}
	// 长度不超过33
	if l < 33 {
		return strings.Repeat("─", l-18) + result
	}
	for i := 1; i <= t60+1; i++ {
		timeStr := strconv.Itoa(i*60) + "s"
		timeStrLen := len(timeStr)
		timeStr = axisFStyle.Render(timeStr)
		targetLen := timeStrLen + i*30
		if l < targetLen {
			// 不渲染标记,仅增加轴长度
			result = strings.Repeat("─", l-lipgloss.Width(result)) + result
			break
		}
		// 渲染标记
liming6's avatar
liming6 committed
59
		result = timeStr + s[0] + strings.Repeat("─", targetLen-lipgloss.Width(result)-timeStrLen-1) + result
liming6's avatar
liming6 committed
60
61
62
63
	}
	return result
}

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
func genXAxisNoStyle(l int) string {
	t60 := l / 30
	t30 := l >= 18
	var result string
	if t30 {
		result = A + strings.Repeat("─", 14)
		result = "30s" + result
	} else {
		return strings.Repeat("─", l)
	}
	// 长度不超过33
	if l < 33 {
		return strings.Repeat("─", l-18) + result
	}
	for i := 1; i <= t60+1; i++ {
		timeStr := strconv.Itoa(i*60) + "s"
		timeStrLen := len(timeStr)
		targetLen := timeStrLen + i*30
		if l < targetLen {
			// 不渲染标记,仅增加轴长度
			result = strings.Repeat("─", l-lipgloss.Width(result)) + result
			break
		}
		// 渲染标记
		result = timeStr + A + strings.Repeat("─", targetLen-lipgloss.Width(result)-timeStrLen-1) + result
	}
	return result
}

liming6's avatar
liming6 committed
93
94
// MyTimeChartMsg 时间流表消息,用于插入数据
type MyTimeChartMsg struct {
95
96
97
98
99
100
101
102
103
	Points []tchart.TimePoint // 待添加的数据点
	Reset  bool               // 添加数据点前是否清除原有数据点
}

func NewTimeCharMsg(point []tchart.TimePoint, reset bool) MyTimeChartMsg {
	return MyTimeChartMsg{
		Points: point,
		Reset:  reset,
	}
liming6's avatar
liming6 committed
104
105
106
107
}

// MyTimeChart 特化的时间流表,时间区域就是宽度的2倍,单位是秒
type MyTimeChart struct {
liming6's avatar
liming6 committed
108
109
110
111
112
113
	chart         *tchart.Model                      // 原始图表
	zM            *zone.Manager                      // 区域管理
	points        *linkedlist.List[tchart.TimePoint] // 数据点,这里只存储put的数据,不存储自动添加的数据
	width, height int                                // 图表的高度和宽度
	max, min      float64                            // y轴的最值
	lockPoints    sync.RWMutex                       // 保护dataSet的并发读写
liming6's avatar
liming6 committed
114
	color         []lipgloss.Color
liming6's avatar
liming6 committed
115
	middleLine    bool // 是否添加中间线
liming6's avatar
liming6 committed
116
117
}

118
// New 新建图表,color的长度如果为1,则图表的颜色为color[0],如果color的长度为heigh,则图表的颜色随高度变化
liming6's avatar
liming6 committed
119
func NewTimeChart(width, height int, vmin, vmax float64, color []lipgloss.Color) *MyTimeChart {
liming6's avatar
liming6 committed
120
121
122
123
124
125
126
	result := MyTimeChart{}
	result.max = vmax
	result.min = vmin
	result.width = width
	result.height = height
	zoneManager := zone.New()
	result.zM = zoneManager
liming6's avatar
liming6 committed
127
128
	result.lockPoints = sync.RWMutex{}
	result.lockPoints.Lock()
liming6's avatar
liming6 committed
129
	result.points = linkedlist.New[tchart.TimePoint]()
liming6's avatar
liming6 committed
130
	result.middleLine = false
liming6's avatar
liming6 committed
131
	now := time.Now()
132
	// 用最小值填充空白点
liming6's avatar
liming6 committed
133
134
135
	t := result.width*2 + 1
	tmpPoints := make([]tchart.TimePoint, 0, t)
	for i := range t {
136
		tmpPoints = append(tmpPoints, tchart.TimePoint{Time: now.Add(time.Duration(-i) * time.Second), Value: result.min})
liming6's avatar
liming6 committed
137
	}
liming6's avatar
liming6 committed
138
	result.lockPoints.Unlock()
liming6's avatar
liming6 committed
139
	s := tchart.New(width, height+1,
liming6's avatar
liming6 committed
140
141
142
143
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(zoneManager),
		tchart.WithYRange(vmin, vmax),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
144
		tchart.WithTimeSeries(tmpPoints),
liming6's avatar
liming6 committed
145
146
	)
	result.chart = &s
147
148
149
150
151
	if len(color) == 2 {
		result.color = GenGradientColor(color[0], color[1], height)
	} else {
		result.color = color
	}
liming6's avatar
liming6 committed
152
153
154
	return &result
}

155
// SortPoints 对不是自动填充的点进行排序
liming6's avatar
liming6 committed
156
157
158
159
160
161
162
163
164
165
166
167
func (m *MyTimeChart) SortPoints() {
	m.points.Sort(func(x, y tchart.TimePoint) int {
		if x.Time.After(y.Time) {
			return 1
		}
		if x.Time.Before(y.Time) {
			return -1
		}
		return 0
	})
}

168
// RemoveUselessPoint 删除无用的点(即超出x轴范围的点)
liming6's avatar
liming6 committed
169
170
func (m *MyTimeChart) RemoveUselessPoint() {
	m.SortPoints()
171
	// th为时间阈值,在此之前的点需要删除
liming6's avatar
liming6 committed
172
173
	th := time.Now().Add(time.Duration(m.width*-2) * time.Second)
	for {
174
		// 如果没有点,则直接退出
liming6's avatar
liming6 committed
175
176
177
178
179
		t, b := m.points.Get(0)
		if !b {
			break
		}
		if t.Time.Before(th) {
180
			// 如果第一个点在阈值之前,就删除
liming6's avatar
liming6 committed
181
182
			m.points.Remove(0)
		} else {
183
			// 否则就退出
liming6's avatar
liming6 committed
184
185
186
187
188
			break
		}
	}
}

189
// PutPoint 添加若干数据点
liming6's avatar
liming6 committed
190
func (m *MyTimeChart) PutPoint(points []tchart.TimePoint) {
191
	m.lockPoints.Lock()
liming6's avatar
liming6 committed
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
	if len(points) > 0 {
		m.points.Add(points...)
	}
	m.RemoveUselessPoint()
	threshold := time.Now().Add(time.Second * time.Duration(-2*m.width-1))
	points = m.points.Values()
	tmpPoint := make([]tchart.TimePoint, 0, m.width*2)
	// 判断是否需要补充空点
	if !points[0].Time.Before(threshold) {
		t := points[0].Time.Add(-time.Second)
		for {
			if !t.Before(threshold) {
				tmpPoint = append(tmpPoint, tchart.TimePoint{Time: t, Value: m.min})
				t = t.Add(-time.Second)
			} else {
				break
			}
liming6's avatar
liming6 committed
209
210
		}
	}
211

liming6's avatar
liming6 committed
212
213
214
215
	tmpPoint = append(tmpPoint, points...)
	sort.Slice(tmpPoint, func(i, j int) bool {
		return tmpPoint[i].Time.Before(tmpPoint[j].Time)
	})
liming6's avatar
liming6 committed
216
	s := tchart.New(m.width, m.height+1,
liming6's avatar
liming6 committed
217
218
219
220
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(m.zM),
		tchart.WithYRange(m.min, m.max),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
221
		tchart.WithTimeSeries(tmpPoint),
liming6's avatar
liming6 committed
222
	)
223
224
225
226
227
228
229
230
231
232
233
234
	m.lockPoints.Unlock()
	// 插入数据
	m.chart = nil
	m.chart = &s
	m.chart.DrawXYAxisAndLabel()
	m.chart.DrawBrailleAll()
}

// ResetPutPoint 清除原有的数据,然后插入数据
func (m *MyTimeChart) ResetPutPoint(points []tchart.TimePoint) {
	// 清除原有的点
	m.lockPoints.Lock()
liming6's avatar
liming6 committed
235
236
237
238
239
240
241
242
	m.points.Clear()
	if len(points) > 0 {
		m.points.Add(points...)
	}
	m.RemoveUselessPoint()
	threshold := time.Now().Add(time.Second * time.Duration(-2*m.width-1))
	points = m.points.Values()
	tmpPoint := make([]tchart.TimePoint, 0, m.width*2)
243

liming6's avatar
liming6 committed
244
245
246
247
248
249
250
	// 判断是否需要补充空点
	if !points[0].Time.Before(threshold) {
		t := points[0].Time.Add(-time.Second)
		for {
			if !t.Before(threshold) {
				tmpPoint = append(tmpPoint, tchart.TimePoint{Time: t, Value: m.min})
				t = t.Add(-time.Second)
251
252
253
254
255
			} else {
				break
			}
		}
	}
liming6's avatar
liming6 committed
256
257
258
259
	tmpPoint = append(tmpPoint, points...)
	sort.Slice(tmpPoint, func(i, j int) bool {
		return tmpPoint[i].Time.Before(tmpPoint[j].Time)
	})
260
261
262
263
264
	s := tchart.New(m.width, m.height+1,
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(m.zM),
		tchart.WithYRange(m.min, m.max),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
265
		tchart.WithTimeSeries(tmpPoint),
266
267
	)
	m.lockPoints.Unlock()
liming6's avatar
liming6 committed
268
269
	// 插入数据
	m.chart = nil
liming6's avatar
liming6 committed
270
	m.chart = &s
liming6's avatar
liming6 committed
271
272
	m.chart.DrawXYAxisAndLabel()
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
273
274
275
276
}

func (m *MyTimeChart) Init() tea.Cmd {
	m.chart.DrawXYAxisAndLabel()
liming6's avatar
liming6 committed
277
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
278
279
280
281
282
283
	return nil
}

func (m *MyTimeChart) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := inputMsg.(type) {
	case MyTimeChartMsg:
284
285
286
287
288
		if msg.Reset {
			m.ResetPutPoint(msg.Points)
		} else {
			m.PutPoint(msg.Points)
		}
liming6's avatar
liming6 committed
289
290
291
292
293
		return m, nil
	}
	return m, nil
}

liming6's avatar
liming6 committed
294
func (m *MyTimeChart) genLines() []string {
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
	str := m.zM.Scan(m.chart.View())
	sb := strings.Builder{}
	sb.WriteString(str[m.width+1:])
	str = sb.String()
	lines := strings.Split(strings.Trim(str, "\n"), "\n")
	runes := make([][]rune, len(lines))
	for k, v := range lines {
		runes[k] = []rune(v)
	}
	width := lipgloss.Width(str)
	height := lipgloss.Height(str)
	for col := range width {
		firstLine := -1
		var charStat utils.CharType = utils.CharEmpty
		for line := range height {
			c := runes[line][col]
			charType := utils.GetCharType(c)
			if charType != utils.CharEmpty && firstLine == -1 {
				firstLine = line
			}
			if firstLine == -1 {
				continue
			}
			if firstLine == line {
				// 遇到了一列的第一个非空字符
				if t, have := utils.MM[c]; have {
					runes[line][col] = t
				}
				charStat = utils.CharTypeOr(charStat, utils.GetCharType(runes[line][col]))
			} else {
				// 第一个非空字符下面的字符
				switch charType {
				case utils.CharEmpty:
					switch charStat {
					case utils.CharLeft:
						runes[line][col] = utils.LeftFullMW
					case utils.CharRight:
						runes[line][col] = utils.RightFullMW
					case utils.CharFull:
						runes[line][col] = utils.FullMW
					}
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
				case utils.CharLeft:
					switch utils.CharTypeOr(charStat, utils.CharLeft) {
					case utils.CharLeft:
						runes[line][col] = utils.LeftFullMW
					case utils.CharRight:
						runes[line][col] = utils.RightFullMW
					case utils.CharFull:
						runes[line][col] = utils.FullMW
					}
					charStat = utils.CharTypeOr(charStat, utils.CharLeft)
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
				case utils.CharRight:
					switch utils.CharTypeOr(charStat, utils.CharRight) {
					case utils.CharLeft:
						runes[line][col] = utils.LeftFullMW
					case utils.CharRight:
						runes[line][col] = utils.RightFullMW
					case utils.CharFull:
						runes[line][col] = utils.FullMW
					}
					charStat = utils.CharTypeOr(charStat, utils.CharRight)
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
				case utils.CharFull:
					charStat = utils.CharFull
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
				}

			}
		}
	}
	resultLines := make([]string, height)
	for k, v := range runes {
		resultLines[k] = string(v)
	}
liming6's avatar
liming6 committed
379
380
381
382
383
384
	return resultLines
}

func (m *MyTimeChart) ViewWithColor(color []lipgloss.Color) string {
	resultLines := m.genLines()
	height := len(resultLines)
liming6's avatar
liming6 committed
385
	style := lipgloss.NewStyle()
386
387
	if len(color) == height {
		for k, v := range resultLines {
liming6's avatar
liming6 committed
388
			resultLines[k] = style.Foreground(color[k]).Render(v)
389
390
391
392
393
		}
	}
	return strings.Join(resultLines, "\n")
}

liming6's avatar
liming6 committed
394
func (m *MyTimeChart) View() string {
liming6's avatar
liming6 committed
395
396
397
398
399
400
401
402
403
404
	resultLines := m.genLines()
	height := len(resultLines)
	switch len(m.color) {
	case 1:
		style := lipgloss.NewStyle().Foreground(m.color[0])
		return style.Render(strings.Join(resultLines, "\n"))
	case height:
		style := lipgloss.NewStyle()
		for k, v := range resultLines {
			resultLines[k] = style.Foreground(m.color[k]).Render(v)
liming6's avatar
liming6 committed
405
		}
liming6's avatar
liming6 committed
406
407
408
		return strings.Join(resultLines, "\n")
	default:
		return strings.Join(resultLines, "\n")
liming6's avatar
liming6 committed
409
	}
liming6's avatar
liming6 committed
410
411
412
413
414
415
416
417
418
419
420
421
422
}

// ViewWithMiddleLine
func (m *MyTimeChart) ViewWithMiddleLine() string {
	resultLines := m.genLines()
	height := len(resultLines)
	targetLine := 0
	isMiddle := false
	if height%2 == 0 {
		targetLine = height/2 - 1
	} else {
		targetLine = height / 2
		isMiddle = true
liming6's avatar
liming6 committed
423
	}
424
425
426
	switch len(m.color) {
	case 1:
		style := lipgloss.NewStyle().Foreground(m.color[0])
liming6's avatar
liming6 committed
427
428
429
430
431
432
433
434
435
436
437
438
439
440
		for k, v := range resultLines {
			if k == targetLine {
				if isMiddle {
					resultLines[k] = style.Strikethrough(true).Render(v)
					style.Strikethrough(false)
				} else {
					resultLines[k] = style.Underline(true).Render(v)
					style.Underline(false)
				}
			} else {
				resultLines[k] = style.Render(v)
			}
		}
		return strings.Join(resultLines, "\n")
441
	case height:
liming6's avatar
liming6 committed
442
		style := lipgloss.NewStyle()
liming6's avatar
liming6 committed
443
		for k, v := range resultLines {
liming6's avatar
liming6 committed
444
445
446
447
448
449
450
451
452
453
454
			if k == targetLine {
				if isMiddle {
					resultLines[k] = style.Foreground(m.color[k]).Strikethrough(true).Render(v)
					style.Strikethrough(false)
				} else {
					resultLines[k] = style.Foreground(m.color[k]).Underline(true).Render(v)
					style.Underline(false)
				}
			} else {
				resultLines[k] = style.Foreground(m.color[k]).Render(v)
			}
liming6's avatar
liming6 committed
455
		}
456
457
		return strings.Join(resultLines, "\n")
	default:
liming6's avatar
liming6 committed
458
459
460
461
462
463
464
465
466
467
468
469
470
471
		style := lipgloss.NewStyle()
		for k, v := range resultLines {
			if k == targetLine {
				if isMiddle {
					resultLines[k] = style.Strikethrough(true).Render(v)
					style.Strikethrough(false)
				} else {
					resultLines[k] = style.Underline(true).Render(v)
					style.Underline(false)
				}
			} else {
				resultLines[k] = style.Render(v)
			}
		}
472
		return strings.Join(resultLines, "\n")
liming6's avatar
liming6 committed
473
	}
liming6's avatar
liming6 committed
474
}