timechart.go 12.8 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
22
23
24
	zone "github.com/lrstanley/bubblezone"
)

const (
	A = "├"
)

var (
25
	axisFStyle = lipgloss.NewStyle().Inline(true).Foreground(lipgloss.Color("#4d4d4dff"))
liming6's avatar
liming6 committed
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
)

// genXAxis 生成X轴,参数l是x轴的长度
func genXAxis(l int) string {
	t60 := l / 30
	t30 := l >= 18
	var result string
	if t30 {
		result = A + strings.Repeat("─", 14)
		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
		}
		// 渲染标记
		result = timeStr + A + strings.Repeat("─", targetLen-lipgloss.Width(result)-timeStrLen-1) + result
	}
	return result
}

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
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
88
89
// MyTimeChartMsg 时间流表消息,用于插入数据
type MyTimeChartMsg struct {
90
91
92
93
94
95
96
97
98
	Points []tchart.TimePoint // 待添加的数据点
	Reset  bool               // 添加数据点前是否清除原有数据点
}

func NewTimeCharMsg(point []tchart.TimePoint, reset bool) MyTimeChartMsg {
	return MyTimeChartMsg{
		Points: point,
		Reset:  reset,
	}
liming6's avatar
liming6 committed
99
100
101
102
}

// MyTimeChart 特化的时间流表,时间区域就是宽度的2倍,单位是秒
type MyTimeChart struct {
liming6's avatar
liming6 committed
103
104
105
106
107
108
	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
109
	color         []lipgloss.Color
liming6's avatar
liming6 committed
110
111
}

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

148
// SortPoints 对不是自动填充的点进行排序
liming6's avatar
liming6 committed
149
150
151
152
153
154
155
156
157
158
159
160
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
	})
}

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

182
// PutPoint 添加若干数据点
liming6's avatar
liming6 committed
183
func (m *MyTimeChart) PutPoint(points []tchart.TimePoint) {
184
	m.lockPoints.Lock()
liming6's avatar
liming6 committed
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
	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
202
203
		}
	}
204

liming6's avatar
liming6 committed
205
206
207
208
	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
209
	s := tchart.New(m.width, m.height+1,
liming6's avatar
liming6 committed
210
211
212
213
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(m.zM),
		tchart.WithYRange(m.min, m.max),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
214
		tchart.WithTimeSeries(tmpPoint),
liming6's avatar
liming6 committed
215
	)
216
217
218
219
220
221
222
223
224
225
226
227
	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
228
229
230
231
232
233
234
235
	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)
236

liming6's avatar
liming6 committed
237
238
239
240
241
242
243
	// 判断是否需要补充空点
	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)
244
245
246
247
248
			} else {
				break
			}
		}
	}
liming6's avatar
liming6 committed
249
250
251
252
	tmpPoint = append(tmpPoint, points...)
	sort.Slice(tmpPoint, func(i, j int) bool {
		return tmpPoint[i].Time.Before(tmpPoint[j].Time)
	})
253
254
255
256
257
	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
258
		tchart.WithTimeSeries(tmpPoint),
259
260
	)
	m.lockPoints.Unlock()
liming6's avatar
liming6 committed
261
262
	// 插入数据
	m.chart = nil
liming6's avatar
liming6 committed
263
	m.chart = &s
liming6's avatar
liming6 committed
264
265
	m.chart.DrawXYAxisAndLabel()
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
266
267
268
269
}

func (m *MyTimeChart) Init() tea.Cmd {
	m.chart.DrawXYAxisAndLabel()
liming6's avatar
liming6 committed
270
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
271
272
273
274
275
276
	return nil
}

func (m *MyTimeChart) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := inputMsg.(type) {
	case MyTimeChartMsg:
277
278
279
280
281
		if msg.Reset {
			m.ResetPutPoint(msg.Points)
		} else {
			m.PutPoint(msg.Points)
		}
liming6's avatar
liming6 committed
282
283
284
285
286
		return m, nil
	}
	return m, nil
}

liming6's avatar
liming6 committed
287
func (m *MyTimeChart) ViewWithColor(color []lipgloss.Color) string {
288
289
290
291
292
293
294
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
	str := m.zM.Scan(m.chart.View())
	sb := strings.Builder{}
	sb.WriteString(str[m.width+1:])
	str = sb.String()
	// return str

	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
374
	style := lipgloss.NewStyle()
375
376
	if len(color) == height {
		for k, v := range resultLines {
liming6's avatar
liming6 committed
377
			resultLines[k] = style.Foreground(color[k]).Render(v)
378
379
380
381
382
		}
	}
	return strings.Join(resultLines, "\n")
}

liming6's avatar
liming6 committed
383
func (m *MyTimeChart) View() string {
liming6's avatar
liming6 committed
384
	str := m.zM.Scan(m.chart.View())
liming6's avatar
liming6 committed
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
	sb := strings.Builder{}
	sb.WriteString(str[m.width+1:])
	str = sb.String()
	// return str

	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
					}
liming6's avatar
liming6 committed
427
428
429
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
430
431
432
433
434
435
436
437
438
439
				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)
liming6's avatar
liming6 committed
440
441
442
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
443
444
445
446
447
448
449
450
451
452
				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)
liming6's avatar
liming6 committed
453
454
455
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
456
457
				case utils.CharFull:
					charStat = utils.CharFull
liming6's avatar
liming6 committed
458
459
460
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
461
462
463
464
465
466
467
468
469
				}

			}
		}
	}
	resultLines := make([]string, height)
	for k, v := range runes {
		resultLines[k] = string(v)
	}
470
471
472
473
474
	switch len(m.color) {
	case 1:
		style := lipgloss.NewStyle().Foreground(m.color[0])
		return style.Render(strings.Join(resultLines, "\n"))
	case height:
liming6's avatar
liming6 committed
475
		style := lipgloss.NewStyle()
liming6's avatar
liming6 committed
476
		for k, v := range resultLines {
liming6's avatar
liming6 committed
477
			resultLines[k] = style.Foreground(m.color[k]).Render(v)
liming6's avatar
liming6 committed
478
		}
479
480
481
		return strings.Join(resultLines, "\n")
	default:
		return strings.Join(resultLines, "\n")
liming6's avatar
liming6 committed
482
	}
liming6's avatar
liming6 committed
483
}