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
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
}

liming6's avatar
liming6 committed
112
// New 新建图表,其中dataSet的Key为数据集名称,value为数据集的颜色
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()
liming6's avatar
liming6 committed
125
126
127
128
	t := result.width*2 + 1
	tmpPoints := make([]tchart.TimePoint, 0, t)
	for i := range t {
		tmpPoints = append(tmpPoints, tchart.TimePoint{Time: now.Add(time.Duration(-i) * time.Second)})
liming6's avatar
liming6 committed
129
	}
liming6's avatar
liming6 committed
130
	result.lockPoints.Unlock()
liming6's avatar
liming6 committed
131
	s := tchart.New(width, height+1,
liming6's avatar
liming6 committed
132
133
134
135
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(zoneManager),
		tchart.WithYRange(vmin, vmax),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
136
		tchart.WithTimeSeries(tmpPoints),
liming6's avatar
liming6 committed
137
138
	)
	result.chart = &s
liming6's avatar
liming6 committed
139
	result.color = color
liming6's avatar
liming6 committed
140
141
142
	return &result
}

liming6's avatar
liming6 committed
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
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
	})
}

func (m *MyTimeChart) RemoveUselessPoint() {
	m.SortPoints()
	th := time.Now().Add(time.Duration(m.width*-2) * time.Second)
	for {
		t, b := m.points.Get(0)
		if !b {
			break
		}
		if t.Time.Before(th) {
			m.points.Remove(0)
		} else {
			break
		}
	}
}

171
// PutPoint 添加若干数据点
liming6's avatar
liming6 committed
172
func (m *MyTimeChart) PutPoint(points []tchart.TimePoint) {
173
	m.lockPoints.Lock()
liming6's avatar
liming6 committed
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
	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
191
192
		}
	}
liming6's avatar
liming6 committed
193
194
195
196
	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
197
	s := tchart.New(m.width, m.height+1,
liming6's avatar
liming6 committed
198
199
200
201
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(m.zM),
		tchart.WithYRange(m.min, m.max),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
202
		tchart.WithTimeSeries(tmpPoint),
liming6's avatar
liming6 committed
203
	)
204
205
206
207
208
209
210
211
212
213
214
215
	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
216
217
218
219
220
221
222
223
	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)
224

liming6's avatar
liming6 committed
225
226
227
228
229
230
231
	// 判断是否需要补充空点
	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)
232
233
234
235
236
			} else {
				break
			}
		}
	}
liming6's avatar
liming6 committed
237
238
239
240
	tmpPoint = append(tmpPoint, points...)
	sort.Slice(tmpPoint, func(i, j int) bool {
		return tmpPoint[i].Time.Before(tmpPoint[j].Time)
	})
241
242
243
244
245
	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
246
		tchart.WithTimeSeries(tmpPoint),
247
248
	)
	m.lockPoints.Unlock()
liming6's avatar
liming6 committed
249
250
	// 插入数据
	m.chart = nil
liming6's avatar
liming6 committed
251
	m.chart = &s
liming6's avatar
liming6 committed
252
253
	m.chart.DrawXYAxisAndLabel()
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
254
255
256
257
}

func (m *MyTimeChart) Init() tea.Cmd {
	m.chart.DrawXYAxisAndLabel()
liming6's avatar
liming6 committed
258
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
259
260
261
262
263
264
	return nil
}

func (m *MyTimeChart) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := inputMsg.(type) {
	case MyTimeChartMsg:
265
266
267
268
269
		if msg.Reset {
			m.ResetPutPoint(msg.Points)
		} else {
			m.PutPoint(msg.Points)
		}
liming6's avatar
liming6 committed
270
271
272
273
274
		return m, nil
	}
	return m, nil
}

liming6's avatar
liming6 committed
275
func (m *MyTimeChart) ViewWithColor(color []lipgloss.Color) string {
276
277
278
279
280
281
282
283
284
285
286
287
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
	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
362
	style := lipgloss.NewStyle()
363
364
	if len(color) == height {
		for k, v := range resultLines {
liming6's avatar
liming6 committed
365
			resultLines[k] = style.Foreground(color[k]).Render(v)
366
367
368
369
370
		}
	}
	return strings.Join(resultLines, "\n")
}

liming6's avatar
liming6 committed
371
func (m *MyTimeChart) View() string {
liming6's avatar
liming6 committed
372
	str := m.zM.Scan(m.chart.View())
liming6's avatar
liming6 committed
373
374
375
376
377
378
379
380
381
382
383
384
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
	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
415
416
417
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
418
419
420
421
422
423
424
425
426
427
				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
428
429
430
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
431
432
433
434
435
436
437
438
439
440
				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
441
442
443
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
444
445
				case utils.CharFull:
					charStat = utils.CharFull
liming6's avatar
liming6 committed
446
447
448
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
449
450
451
452
453
454
455
456
457
458
				}

			}
		}
	}
	resultLines := make([]string, height)
	for k, v := range runes {
		resultLines[k] = string(v)
	}
	if len(m.color) == height {
liming6's avatar
liming6 committed
459
		style := lipgloss.NewStyle()
liming6's avatar
liming6 committed
460
		for k, v := range resultLines {
liming6's avatar
liming6 committed
461
			resultLines[k] = style.Foreground(m.color[k]).Render(v)
liming6's avatar
liming6 committed
462
		}
liming6's avatar
liming6 committed
463
464
465
	} else if len(m.color) == 1 {
		style := lipgloss.NewStyle().Foreground(m.color[0])
		return style.Render(strings.Join(resultLines, "\n"))
liming6's avatar
liming6 committed
466
467
	}
	return strings.Join(resultLines, "\n")
liming6's avatar
liming6 committed
468
}