bump dependencies
parent
8c34a5d9f7
commit
ccca53914d
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2018, Rohit Gupta
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,138 @@
|
||||
package asciigraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Plot returns ascii graph for a series.
|
||||
func Plot(series []float64, options ...Option) string {
|
||||
config := configure(config{
|
||||
Offset: 3,
|
||||
}, options)
|
||||
|
||||
if config.Width > 0 {
|
||||
series = interpolateArray(series, config.Width)
|
||||
}
|
||||
|
||||
minimum, maximum := minMaxFloat64Slice(series)
|
||||
interval := math.Abs(maximum - minimum)
|
||||
|
||||
if config.Height <= 0 {
|
||||
if int(interval) <= 0 {
|
||||
config.Height = int(interval * math.Pow10(int(math.Ceil(-math.Log10(interval)))))
|
||||
} else {
|
||||
config.Height = int(interval)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Offset <= 0 {
|
||||
config.Offset = 3
|
||||
}
|
||||
|
||||
ratio := float64(config.Height) / interval
|
||||
min2 := round(minimum * ratio)
|
||||
max2 := round(maximum * ratio)
|
||||
|
||||
intmin2 := int(min2)
|
||||
intmax2 := int(max2)
|
||||
|
||||
rows := int(math.Abs(float64(intmax2 - intmin2)))
|
||||
width := len(series) + config.Offset
|
||||
|
||||
var plot [][]string
|
||||
|
||||
// initialise empty 2D grid
|
||||
for i := 0; i < rows+1; i++ {
|
||||
var line []string
|
||||
for j := 0; j < width; j++ {
|
||||
line = append(line, " ")
|
||||
}
|
||||
plot = append(plot, line)
|
||||
}
|
||||
|
||||
precision := 2
|
||||
logMaximum := math.Log10(math.Max(math.Abs(maximum), math.Abs(minimum))) //to find number of zeros after decimal
|
||||
|
||||
if logMaximum < 0 {
|
||||
// negative log
|
||||
if math.Mod(logMaximum, 1) != 0 {
|
||||
// non-zero digits after decimal
|
||||
precision = precision + int(math.Abs(logMaximum))
|
||||
} else {
|
||||
precision = precision + int(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++ {
|
||||
label := fmt.Sprintf("%*.*f", maxWidth+1, precision, maximum-(float64(y-intmin2)*interval/float64(rows)))
|
||||
w := y - intmin2
|
||||
h := int(math.Max(float64(config.Offset)-float64(len(label)), 0))
|
||||
|
||||
plot[w][h] = label
|
||||
if y == 0 {
|
||||
plot[w][config.Offset-1] = "┼"
|
||||
} else {
|
||||
plot[w][config.Offset-1] = "┤"
|
||||
}
|
||||
}
|
||||
|
||||
y0 := int(round(series[0]*ratio) - min2)
|
||||
var y1 int
|
||||
|
||||
plot[rows-y0][config.Offset-1] = "┼" // first value
|
||||
|
||||
for x := 0; x < len(series)-1; x++ { // plot the line
|
||||
y0 = int(round(series[x+0]*ratio) - float64(intmin2))
|
||||
y1 = int(round(series[x+1]*ratio) - float64(intmin2))
|
||||
if y0 == y1 {
|
||||
plot[rows-y0][x+config.Offset] = "─"
|
||||
} else {
|
||||
if y0 > y1 {
|
||||
plot[rows-y1][x+config.Offset] = "╰"
|
||||
} else {
|
||||
plot[rows-y1][x+config.Offset] = "╭"
|
||||
}
|
||||
if y0 > y1 {
|
||||
plot[rows-y0][x+config.Offset] = "╮"
|
||||
} else {
|
||||
plot[rows-y0][x+config.Offset] = "╯"
|
||||
}
|
||||
|
||||
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] = "│"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// join columns
|
||||
var lines bytes.Buffer
|
||||
for h, horizontal := range plot {
|
||||
if h != 0 {
|
||||
lines.WriteRune('\n')
|
||||
}
|
||||
for _, v := range horizontal {
|
||||
lines.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
// add caption if not empty
|
||||
if config.Caption != "" {
|
||||
lines.WriteRune('\n')
|
||||
lines.WriteString(strings.Repeat(" ", config.Offset+maxWidth+2))
|
||||
lines.WriteString(config.Caption)
|
||||
}
|
||||
|
||||
return lines.String()
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package asciigraph
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Option represents a configuration setting.
|
||||
type Option interface {
|
||||
apply(c *config)
|
||||
}
|
||||
|
||||
// config holds various graph options
|
||||
type config struct {
|
||||
Width, Height int
|
||||
Offset int
|
||||
Caption string
|
||||
}
|
||||
|
||||
// An optionFunc applies an option.
|
||||
type optionFunc func(*config)
|
||||
|
||||
// apply implements the Option interface.
|
||||
func (of optionFunc) apply(c *config) { of(c) }
|
||||
|
||||
func configure(defaults config, options []Option) *config {
|
||||
for _, o := range options {
|
||||
o.apply(&defaults)
|
||||
}
|
||||
return &defaults
|
||||
}
|
||||
|
||||
// Width sets the graphs width. By default, the width of the graph is
|
||||
// determined by the number of data points. If the value given is a
|
||||
// positive number, the data points are interpolated on the x axis.
|
||||
// Values <= 0 reset the width to the default value.
|
||||
func Width(w int) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
if w > 0 {
|
||||
c.Width = w
|
||||
} else {
|
||||
c.Width = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Height sets the graphs height.
|
||||
func Height(h int) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
if h > 0 {
|
||||
c.Height = h
|
||||
} else {
|
||||
c.Height = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Offset sets the graphs offset.
|
||||
func Offset(o int) Option {
|
||||
return optionFunc(func(c *config) { c.Offset = o })
|
||||
}
|
||||
|
||||
// Caption sets the graphs caption.
|
||||
func Caption(caption string) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
c.Caption = strings.TrimSpace(caption)
|
||||
})
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package asciigraph
|
||||
|
||||
import "math"
|
||||
|
||||
func minMaxFloat64Slice(v []float64) (min, max float64) {
|
||||
min = math.Inf(1)
|
||||
max = math.Inf(-1)
|
||||
|
||||
if len(v) == 0 {
|
||||
panic("Empty slice")
|
||||
}
|
||||
|
||||
for _, e := range v {
|
||||
if e < min {
|
||||
min = e
|
||||
}
|
||||
if e > max {
|
||||
max = e
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func round(input float64) float64 {
|
||||
if math.IsNaN(input) {
|
||||
return math.NaN()
|
||||
}
|
||||
sign := 1.0
|
||||
if input < 0 {
|
||||
sign = -1
|
||||
input *= -1
|
||||
}
|
||||
_, decimal := math.Modf(input)
|
||||
var rounded float64
|
||||
if decimal >= 0.5 {
|
||||
rounded = math.Ceil(input)
|
||||
} else {
|
||||
rounded = math.Floor(input)
|
||||
}
|
||||
return rounded * sign
|
||||
}
|
||||
|
||||
func linearInterpolate(before, after, atPoint float64) float64 {
|
||||
return before + (after-before)*atPoint
|
||||
}
|
||||
|
||||
func interpolateArray(data []float64, fitCount int) []float64 {
|
||||
|
||||
var interpolatedData []float64
|
||||
|
||||
springFactor := float64(len(data)-1) / float64(fitCount-1)
|
||||
interpolatedData = append(interpolatedData, data[0])
|
||||
|
||||
for i := 1; i < fitCount-1; i++ {
|
||||
spring := float64(i) * springFactor
|
||||
before := math.Floor(spring)
|
||||
after := math.Ceil(spring)
|
||||
atPoint := spring - before
|
||||
interpolatedData = append(interpolatedData, linearInterpolate(data[int(before)], data[int(after)], atPoint))
|
||||
}
|
||||
interpolatedData = append(interpolatedData, data[len(data)-1])
|
||||
return interpolatedData
|
||||
}
|
Loading…
Reference in New Issue