podman
329 строк · 5.9 Кб
1package goterm2
3import (4"fmt"5"math"6"strings"7"unicode/utf8"8)
9
10const (11AXIS_LEFT = iota12AXIS_RIGHT
13)
14
15const (16DRAW_INDEPENDENT = 1 << iota17DRAW_RELATIVE
18)
19
20type DataTable struct {21columns []string22
23rows [][]float6424}
25
26func (d *DataTable) AddColumn(name string) {27d.columns = append(d.columns, name)28}
29
30func (d *DataTable) AddRow(elms ...float64) {31d.rows = append(d.rows, elms)32}
33
34type Chart interface {35Draw(data DataTable, flags int) string36}
37
38type LineChart struct {39Buf []string40chartBuf []string41
42data *DataTable43
44Width int45Height int46
47chartHeight int48chartWidth int49
50paddingX int51
52paddingY int53
54Flags int55}
56
57func genBuf(size int) []string {58buf := make([]string, size)59
60for i := 0; i < size; i++ {61buf[i] = " "62}63
64return buf65}
66
67// Format float
68func ff(num interface{}) string {69return fmt.Sprintf("%.1f", num)70}
71
72func NewLineChart(width, height int) *LineChart {73chart := new(LineChart)74chart.Width = width75chart.Height = height76chart.Buf = genBuf(width * height)77
78// axis lines + axies text79chart.paddingY = 280
81return chart82}
83
84func (c *LineChart) DrawAxes(maxX, minX, maxY, minY float64, index int) {85side := AXIS_LEFT86
87if c.Flags&DRAW_INDEPENDENT != 0 {88if index%2 == 0 {89side = AXIS_RIGHT90}91
92c.DrawLine(c.paddingX-1, 1, c.Width-c.paddingX, 1, "-")93} else {94c.DrawLine(c.paddingX-1, 1, c.Width-1, 1, "-")95}96
97if side == AXIS_LEFT {98c.DrawLine(c.paddingX-1, 1, c.paddingX-1, c.Height-1, "│")99} else {100c.DrawLine(c.Width-c.paddingX, 1, c.Width-c.paddingX, c.Height-1, "│")101}102
103left := 0104if side == AXIS_RIGHT {105left = c.Width - c.paddingX + 1106}107
108if c.Flags&DRAW_RELATIVE != 0 {109c.writeText(ff(minY), left, 1)110} else {111if minY > 0 {112c.writeText("0", left, 1)113} else {114c.writeText(ff(minY), left, 1)115}116}117
118c.writeText(ff(maxY), left, c.Height-1)119
120c.writeText(ff(minX), c.paddingX, 0)121
122x_col := c.data.columns[0]123c.writeText(c.data.columns[0], c.Width/2-utf8.RuneCountInString(x_col)/2, 1)124
125if c.Flags&DRAW_INDEPENDENT != 0 || len(c.data.columns) < 3 {126col := c.data.columns[index]127
128for idx, char := range strings.Split(col, "") {129start_from := c.Height/2 + len(col)/2 - idx130
131if side == AXIS_LEFT {132c.writeText(char, c.paddingX-1, start_from)133} else {134c.writeText(char, c.Width-c.paddingX, start_from)135}136}137}138
139if c.Flags&DRAW_INDEPENDENT != 0 {140c.writeText(ff(maxX), c.Width-c.paddingX-len(ff(maxX)), 0)141} else {142c.writeText(ff(maxX), c.Width-len(ff(maxX)), 0)143}144}
145
146func (c *LineChart) writeText(text string, x, y int) {147coord := y*c.Width + x148
149for idx, char := range strings.Split(text, "") {150c.Buf[coord+idx] = char151}152}
153
154func (c *LineChart) Draw(data *DataTable) (out string) {155var scaleY, scaleX float64156
157c.data = data158
159if c.Flags&DRAW_INDEPENDENT != 0 && len(data.columns) > 3 {160fmt.Println("Error: Can't use DRAW_INDEPENDENT for more then 2 graphs")161return ""162}163
164charts := len(data.columns) - 1165
166prevPoint := [2]int{-1, -1}167
168maxX, minX, maxY, minY := getBoundaryValues(data, -1)169
170c.paddingX = int(math.Max(float64(len(ff(minY))), float64(len(ff(maxY))))) + 1171
172c.chartHeight = c.Height - c.paddingY173
174if c.Flags&DRAW_INDEPENDENT != 0 {175c.chartWidth = c.Width - 2*c.paddingX176} else {177c.chartWidth = c.Width - c.paddingX - 1178}179
180scaleX = float64(c.chartWidth) / (maxX - minX)181
182if c.Flags&DRAW_RELATIVE != 0 || minY < 0 {183scaleY = float64(c.chartHeight) / (maxY - minY)184} else {185scaleY = float64(c.chartHeight) / maxY186}187
188for i := 1; i < charts+1; i++ {189if c.Flags&DRAW_INDEPENDENT != 0 {190maxX, minX, maxY, minY = getBoundaryValues(data, i)191
192scaleX = float64(c.chartWidth-1) / (maxX - minX)193scaleY = float64(c.chartHeight) / maxY194
195if c.Flags&DRAW_RELATIVE != 0 || minY < 0 {196scaleY = float64(c.chartHeight) / (maxY - minY)197}198}199
200symbol := Color("•", i)201
202c_data := getChartData(data, i)203
204for _, point := range c_data {205x := int((point[0]-minX)*scaleX) + c.paddingX206y := int((point[1])*scaleY) + c.paddingY207
208if c.Flags&DRAW_RELATIVE != 0 || minY < 0 {209y = int((point[1]-minY)*scaleY) + c.paddingY210}211
212if prevPoint[0] == -1 {213prevPoint[0] = x214prevPoint[1] = y215}216
217if prevPoint[0] <= x {218c.DrawLine(prevPoint[0], prevPoint[1], x, y, symbol)219}220
221prevPoint[0] = x222prevPoint[1] = y223}224
225c.DrawAxes(maxX, minX, maxY, minY, i)226}227
228for row := c.Height - 1; row >= 0; row-- {229out += strings.Join(c.Buf[row*c.Width:(row+1)*c.Width], "") + "\n"230}231
232return233}
234
235func (c *LineChart) DrawLine(x0, y0, x1, y1 int, symbol string) {236drawLine(x0, y0, x1, y1, func(x, y int) {237coord := y*c.Width + x238
239if coord > 0 && coord < len(c.Buf) {240c.Buf[coord] = symbol241}242})243}
244
245func getBoundaryValues(data *DataTable, index int) (maxX, minX, maxY, minY float64) {246maxX = math.Inf(-1)247minX = math.Inf(1)248maxY = math.Inf(-1)249minY = math.Inf(1)250
251for _, r := range data.rows {252maxX = math.Max(maxX, r[0])253minX = math.Min(minX, r[0])254
255for idx, c := range r {256if idx > 0 {257if index == -1 || index == idx {258maxY = math.Max(maxY, c)259minY = math.Min(minY, c)260}261}262}263}264
265if maxY > 0 {266maxY = maxY * 1.1267} else {268maxY = maxY * 0.9269}270
271if minY > 0 {272minY = minY * 0.9273} else {274minY = minY * 1.1275}276
277return278}
279
280// DataTable can contain data for multiple graphs, we need to extract only 1
281func getChartData(data *DataTable, index int) (out [][]float64) {282for _, r := range data.rows {283out = append(out, []float64{r[0], r[index]})284}285
286return287}
288
289// Algorithm for drawing line between two points
290//
291// http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
292func drawLine(x0, y0, x1, y1 int, plot func(int, int)) {293dx := x1 - x0294if dx < 0 {295dx = -dx296}297dy := y1 - y0298if dy < 0 {299dy = -dy300}301var sx, sy int302if x0 < x1 {303sx = 1304} else {305sx = -1306}307if y0 < y1 {308sy = 1309} else {310sy = -1311}312err := dx - dy313
314for {315plot(x0, y0)316if x0 == x1 && y0 == y1 {317break318}319e2 := 2 * err320if e2 > -dy {321err -= dy322x0 += sx323}324if e2 < dx {325err += dx326y0 += sy327}328}329}
330