265
vendor/github.com/guptarohit/asciigraph/asciigraph.go
generated
vendored
Normal file
265
vendor/github.com/guptarohit/asciigraph/asciigraph.go
generated
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
package asciigraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Plot returns ascii graph for a series.
|
||||
func Plot(series []float64, options ...Option) string {
|
||||
return PlotMany([][]float64{series}, options...)
|
||||
}
|
||||
|
||||
// PlotMany returns ascii graph for multiple series.
|
||||
func PlotMany(data [][]float64, options ...Option) string {
|
||||
var logMaximum float64
|
||||
config := configure(config{
|
||||
Offset: 3,
|
||||
Precision: 2,
|
||||
}, options)
|
||||
|
||||
// Create a deep copy of the input data
|
||||
dataCopy := make([][]float64, len(data))
|
||||
for i, series := range data {
|
||||
dataCopy[i] = make([]float64, len(series))
|
||||
copy(dataCopy[i], series)
|
||||
}
|
||||
data = dataCopy
|
||||
|
||||
lenMax := 0
|
||||
for i := range data {
|
||||
if l := len(data[i]); l > lenMax {
|
||||
lenMax = l
|
||||
}
|
||||
}
|
||||
|
||||
if config.Width > 0 {
|
||||
for i := range data {
|
||||
for j := len(data[i]); j < lenMax; j++ {
|
||||
data[i] = append(data[i], math.NaN())
|
||||
}
|
||||
data[i] = interpolateArray(data[i], config.Width)
|
||||
}
|
||||
|
||||
lenMax = config.Width
|
||||
}
|
||||
|
||||
minimum, maximum := math.Inf(1), math.Inf(-1)
|
||||
for i := range data {
|
||||
minVal, maxVal := minMaxFloat64Slice(data[i])
|
||||
if minVal < minimum {
|
||||
minimum = minVal
|
||||
}
|
||||
if maxVal > maximum {
|
||||
maximum = maxVal
|
||||
}
|
||||
}
|
||||
if config.LowerBound != nil && *config.LowerBound < minimum {
|
||||
minimum = *config.LowerBound
|
||||
}
|
||||
if config.UpperBound != nil && *config.UpperBound > maximum {
|
||||
maximum = *config.UpperBound
|
||||
}
|
||||
interval := math.Abs(maximum - minimum)
|
||||
|
||||
if config.Height <= 0 {
|
||||
config.Height = calculateHeight(interval)
|
||||
}
|
||||
|
||||
if config.Offset <= 0 {
|
||||
config.Offset = 3
|
||||
}
|
||||
|
||||
var ratio float64
|
||||
if interval != 0 {
|
||||
ratio = float64(config.Height) / interval
|
||||
} else {
|
||||
ratio = 1
|
||||
}
|
||||
min2 := round(minimum * ratio)
|
||||
max2 := round(maximum * ratio)
|
||||
|
||||
intmin2 := int(min2)
|
||||
intmax2 := int(max2)
|
||||
|
||||
rows := int(math.Abs(float64(intmax2 - intmin2)))
|
||||
width := lenMax + config.Offset
|
||||
|
||||
type cell struct {
|
||||
Text string
|
||||
Color AnsiColor
|
||||
}
|
||||
plot := make([][]cell, rows+1)
|
||||
|
||||
// initialise empty 2D grid
|
||||
for i := 0; i < rows+1; i++ {
|
||||
line := make([]cell, width)
|
||||
for j := 0; j < width; j++ {
|
||||
line[j].Text = " "
|
||||
line[j].Color = Default
|
||||
}
|
||||
plot[i] = line
|
||||
}
|
||||
|
||||
precision := config.Precision
|
||||
logMaximum = math.Log10(math.Max(math.Abs(maximum), math.Abs(minimum))) //to find number of zeros after decimal
|
||||
if minimum == float64(0) && maximum == float64(0) {
|
||||
logMaximum = float64(-1)
|
||||
}
|
||||
|
||||
if logMaximum < 0 {
|
||||
// negative log
|
||||
if math.Mod(logMaximum, 1) != 0 {
|
||||
// non-zero digits after decimal
|
||||
precision += uint(math.Abs(logMaximum))
|
||||
} else {
|
||||
precision += uint(math.Abs(logMaximum) - 1.0)
|
||||
}
|
||||
} else if logMaximum > 2 {
|
||||
precision = 0
|
||||
}
|
||||
|
||||
maxNumLength := len(fmt.Sprintf("%0.*f", precision, maximum))
|
||||
minNumLength := len(fmt.Sprintf("%0.*f", precision, minimum))
|
||||
maxWidth := int(math.Max(float64(maxNumLength), float64(minNumLength)))
|
||||
|
||||
// axis and labels
|
||||
for y := intmin2; y < intmax2+1; y++ {
|
||||
var magnitude float64
|
||||
if rows > 0 {
|
||||
magnitude = maximum - (float64(y-intmin2) * interval / float64(rows))
|
||||
} else {
|
||||
magnitude = float64(y)
|
||||
}
|
||||
|
||||
label := fmt.Sprintf("%*.*f", maxWidth+1, precision, magnitude)
|
||||
w := y - intmin2
|
||||
h := int(math.Max(float64(config.Offset)-float64(len(label)), 0))
|
||||
|
||||
plot[w][h].Text = label
|
||||
plot[w][h].Color = config.LabelColor
|
||||
plot[w][config.Offset-1].Text = "┤"
|
||||
plot[w][config.Offset-1].Color = config.AxisColor
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
series := data[i]
|
||||
|
||||
color := Default
|
||||
if i < len(config.SeriesColors) {
|
||||
color = config.SeriesColors[i]
|
||||
}
|
||||
|
||||
var y0, y1 int
|
||||
|
||||
if !math.IsNaN(series[0]) {
|
||||
y0 = int(round(series[0]*ratio) - min2)
|
||||
plot[rows-y0][config.Offset-1].Text = "┼" // first value
|
||||
plot[rows-y0][config.Offset-1].Color = config.AxisColor
|
||||
}
|
||||
|
||||
for x := 0; x < len(series)-1; x++ { // plot the line
|
||||
d0 := series[x]
|
||||
d1 := series[x+1]
|
||||
|
||||
if math.IsNaN(d0) && math.IsNaN(d1) {
|
||||
continue
|
||||
}
|
||||
|
||||
if math.IsNaN(d1) && !math.IsNaN(d0) {
|
||||
y0 = int(round(d0*ratio) - float64(intmin2))
|
||||
plot[rows-y0][x+config.Offset].Text = "╴"
|
||||
plot[rows-y0][x+config.Offset].Color = color
|
||||
continue
|
||||
}
|
||||
|
||||
if math.IsNaN(d0) && !math.IsNaN(d1) {
|
||||
y1 = int(round(d1*ratio) - float64(intmin2))
|
||||
plot[rows-y1][x+config.Offset].Text = "╶"
|
||||
plot[rows-y1][x+config.Offset].Color = color
|
||||
continue
|
||||
}
|
||||
|
||||
y0 = int(round(d0*ratio) - float64(intmin2))
|
||||
y1 = int(round(d1*ratio) - float64(intmin2))
|
||||
|
||||
if y0 == y1 {
|
||||
plot[rows-y0][x+config.Offset].Text = "─"
|
||||
} else {
|
||||
if y0 > y1 {
|
||||
plot[rows-y1][x+config.Offset].Text = "╰"
|
||||
plot[rows-y0][x+config.Offset].Text = "╮"
|
||||
} else {
|
||||
plot[rows-y1][x+config.Offset].Text = "╭"
|
||||
plot[rows-y0][x+config.Offset].Text = "╯"
|
||||
}
|
||||
|
||||
start := int(math.Min(float64(y0), float64(y1))) + 1
|
||||
end := int(math.Max(float64(y0), float64(y1)))
|
||||
for y := start; y < end; y++ {
|
||||
plot[rows-y][x+config.Offset].Text = "│"
|
||||
}
|
||||
}
|
||||
|
||||
start := int(math.Min(float64(y0), float64(y1)))
|
||||
end := int(math.Max(float64(y0), float64(y1)))
|
||||
for y := start; y <= end; y++ {
|
||||
plot[rows-y][x+config.Offset].Color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// join columns
|
||||
var lines bytes.Buffer
|
||||
for h, horizontal := range plot {
|
||||
if h != 0 {
|
||||
lines.WriteRune('\n')
|
||||
}
|
||||
|
||||
// remove trailing spaces
|
||||
lastCharIndex := 0
|
||||
for i := width - 1; i >= 0; i-- {
|
||||
if horizontal[i].Text != " " {
|
||||
lastCharIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c := Default
|
||||
for _, v := range horizontal[:lastCharIndex+1] {
|
||||
if v.Color != c {
|
||||
c = v.Color
|
||||
lines.WriteString(c.String())
|
||||
}
|
||||
|
||||
lines.WriteString(v.Text)
|
||||
}
|
||||
if c != Default {
|
||||
lines.WriteString(Default.String())
|
||||
}
|
||||
}
|
||||
|
||||
// add caption if not empty
|
||||
if config.Caption != "" {
|
||||
lines.WriteRune('\n')
|
||||
lines.WriteString(strings.Repeat(" ", config.Offset+maxWidth))
|
||||
if len(config.Caption) < lenMax {
|
||||
lines.WriteString(strings.Repeat(" ", (lenMax-len(config.Caption))/2))
|
||||
}
|
||||
if config.CaptionColor != Default {
|
||||
lines.WriteString(config.CaptionColor.String())
|
||||
}
|
||||
lines.WriteString(config.Caption)
|
||||
if config.CaptionColor != Default {
|
||||
lines.WriteString(Default.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.SeriesLegends) > 0 {
|
||||
addLegends(&lines, config, lenMax, config.Offset+maxWidth)
|
||||
}
|
||||
|
||||
return lines.String()
|
||||
}
|
||||
Reference in New Issue
Block a user