timechart.go 11.6 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
16
17
18
19
20
21
22
23
	"github.com/NimbleMarkets/ntcharts/canvas/runes"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	zone "github.com/lrstanley/bubblezone"
)

const (
	A = "├"
)

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

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

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

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

// MyTimeChart 特化的时间流表,时间区域就是宽度的2倍,单位是秒
type MyTimeChart struct {
liming6's avatar
liming6 committed
102
103
104
105
106
107
	chart         *tchart.Model      // 原始图表
	zM            *zone.Manager      // 区域管理
	points        []tchart.TimePoint // 数据点
	width, height int                // 图表的高度和宽度
	max, min      float64            // y轴的最值
	lockPoints    sync.RWMutex       // 保护dataSet的并发读写
liming6's avatar
liming6 committed
108
	color         []lipgloss.Color
liming6's avatar
liming6 committed
109
110
}

liming6's avatar
liming6 committed
111
// New 新建图表,其中dataSet的Key为数据集名称,value为数据集的颜色
liming6's avatar
liming6 committed
112
func NewTimeChart(width, height int, vmin, vmax float64, color []lipgloss.Color) *MyTimeChart {
liming6's avatar
liming6 committed
113
114
115
116
117
118
119
	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
120
121
122
	result.lockPoints = sync.RWMutex{}
	result.lockPoints.Lock()
	result.points = make([]tchart.TimePoint, (result.width*2)+1)
liming6's avatar
liming6 committed
123
124
125
	// 准备数据,数据间隔为1秒
	now := time.Now()
	for i := result.width * 2; i >= 0; i-- {
liming6's avatar
liming6 committed
126
		result.points[i] = tchart.TimePoint{Time: now.Add(time.Duration(-i) * time.Second), Value: vmin}
liming6's avatar
liming6 committed
127
	}
liming6's avatar
liming6 committed
128
	result.lockPoints.Unlock()
liming6's avatar
liming6 committed
129
	s := tchart.New(width, height+1,
liming6's avatar
liming6 committed
130
131
132
133
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(zoneManager),
		tchart.WithYRange(vmin, vmax),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
134
		tchart.WithTimeSeries(result.points),
liming6's avatar
liming6 committed
135
136
	)
	result.chart = &s
liming6's avatar
liming6 committed
137
	result.color = color
liming6's avatar
liming6 committed
138
139
140
	return &result
}

141
// PutPoint 添加若干数据点
liming6's avatar
liming6 committed
142
func (m *MyTimeChart) PutPoint(points []tchart.TimePoint) {
143
	m.lockPoints.Lock()
liming6's avatar
liming6 committed
144
145
146
147
148
149
150
151
152
153
154
155
156
	ops := append(m.points, points...)
	// 排序
	sort.Slice(ops, func(i, j int) bool {
		return ops[i].Time.Before(ops[j].Time)
	})
	threshold := time.Now().Add(time.Second * time.Duration(-2*m.width))
	targetIndex := 0
	for i, p := range ops {
		if !p.Time.Before(threshold) {
			targetIndex = i
			break
		}
	}
157
	nps := append(make([]tchart.TimePoint, 0, targetIndex+1), ops[targetIndex:]...)
liming6's avatar
liming6 committed
158
	m.points = nps
liming6's avatar
liming6 committed
159
	s := tchart.New(m.width, m.height+1,
liming6's avatar
liming6 committed
160
161
162
163
		tchart.WithLineStyle(runes.ThinLineStyle),
		tchart.WithZoneManager(m.zM),
		tchart.WithYRange(m.min, m.max),
		tchart.WithXYSteps(0, 0),
liming6's avatar
liming6 committed
164
		tchart.WithTimeSeries(m.points),
liming6's avatar
liming6 committed
165
	)
166
167
168
169
170
171
172
173
174
175
176
177
	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
178
	m.points = m.points[:0]
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220

	// 排序输入数据
	sort.Slice(points, func(i, j int) bool {
		return points[i].Time.Before(points[j].Time)
	})
	// 补充数据 或 剔除数据
	now := time.Now()
	threshold := now.Add(time.Duration(m.width*-2) * time.Second)
	first := points[0].Time
	if first.Before(threshold) {
		// 需要剔除数据
		index := 0
		for k, v := range points {
			if v.Time.Before(threshold) {
				continue
			} else {
				index = k
				break
			}
		}
		m.points = append(make([]tchart.TimePoint, 0, len(points[index:])), points[index:]...)
	} else if first.After(threshold) {
		// 需要补充数据
		max := m.width * 2
		for i := 1; i < max; i++ {
			ta := now.Add(time.Second * time.Duration(-i))
			if !ta.Before(threshold) {
				m.points = append(m.points, tchart.TimePoint{Time: ta, Value: m.min})
			} else {
				break
			}
		}
		m.points = append(m.points, points...)
	}
	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),
		tchart.WithTimeSeries(m.points),
	)
	m.lockPoints.Unlock()
liming6's avatar
liming6 committed
221
222
	// 插入数据
	m.chart = nil
liming6's avatar
liming6 committed
223
	m.chart = &s
liming6's avatar
liming6 committed
224
225
	m.chart.DrawXYAxisAndLabel()
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
226
227
228
229
}

func (m *MyTimeChart) Init() tea.Cmd {
	m.chart.DrawXYAxisAndLabel()
liming6's avatar
liming6 committed
230
	m.chart.DrawBrailleAll()
liming6's avatar
liming6 committed
231
232
233
234
235
236
	return nil
}

func (m *MyTimeChart) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := inputMsg.(type) {
	case MyTimeChartMsg:
237
238
239
240
241
		if msg.Reset {
			m.ResetPutPoint(msg.Points)
		} else {
			m.PutPoint(msg.Points)
		}
liming6's avatar
liming6 committed
242
243
244
245
246
		return m, nil
	}
	return m, nil
}

liming6's avatar
liming6 committed
247
func (m *MyTimeChart) ViewWithColor(color []lipgloss.Color) string {
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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
	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
334
	style := lipgloss.NewStyle()
335
336
	if len(color) == height {
		for k, v := range resultLines {
liming6's avatar
liming6 committed
337
			resultLines[k] = style.Foreground(color[k]).Render(v)
338
339
340
341
342
		}
	}
	return strings.Join(resultLines, "\n")
}

liming6's avatar
liming6 committed
343
func (m *MyTimeChart) View() string {
liming6's avatar
liming6 committed
344
	str := m.zM.Scan(m.chart.View())
liming6's avatar
liming6 committed
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
379
380
381
382
383
384
385
386
	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
387
388
389
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
390
391
392
393
394
395
396
397
398
399
				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
400
401
402
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
403
404
405
406
407
408
409
410
411
412
				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
413
414
415
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
416
417
				case utils.CharFull:
					charStat = utils.CharFull
liming6's avatar
liming6 committed
418
419
420
					if t, have := utils.MM[runes[line][col]]; have {
						runes[line][col] = t
					}
liming6's avatar
liming6 committed
421
422
423
424
425
426
427
428
429
430
				}

			}
		}
	}
	resultLines := make([]string, height)
	for k, v := range runes {
		resultLines[k] = string(v)
	}
	if len(m.color) == height {
liming6's avatar
liming6 committed
431
		style := lipgloss.NewStyle()
liming6's avatar
liming6 committed
432
		for k, v := range resultLines {
liming6's avatar
liming6 committed
433
			resultLines[k] = style.Foreground(m.color[k]).Render(v)
liming6's avatar
liming6 committed
434
		}
liming6's avatar
liming6 committed
435
436
437
	} 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
438
439
	}
	return strings.Join(resultLines, "\n")
liming6's avatar
liming6 committed
440
}