Skip to content

Commit

Permalink
Merge 9537a7c into ac93c63
Browse files Browse the repository at this point in the history
  • Loading branch information
dmke committed Jun 26, 2018
2 parents ac93c63 + 9537a7c commit 2fec436
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 99 deletions.
32 changes: 28 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ Basic graph
func main() {
data := []float64{3, 4, 9, 6, 2, 4, 5, 8, 5, 10, 2, 7, 2, 5, 6}
conf := map[string]interface{}{}
graph := asciigraph.Plot(data, conf)
graph := asciigraph.Plot(data)
fmt.Println(graph)
}
Expand All @@ -78,6 +75,33 @@ Running this example would render the following graph:
2.00 ┤ ╰╯ ╰╯╰╯
..
Command line interface
----------------------

This package also brings a small utility for command line usage. Assuming
`$GOPATH/bin` is in yout `$PATH`, simply `go get` it, and feed it data
points via stdin:

::

$ go install github.com/guptarohit/asciigraph/cmd/asciigraph
$ seq 1 72 | asciigraph -h 10 -c "plot data from stdin"
72.00 ┼
65.55 ┤ ╭────
59.09 ┤ ╭──────╯
52.64 ┤ ╭──────╯
46.18 ┤ ╭──────╯
39.73 ┤ ╭──────╯
33.27 ┤ ╭───────╯
26.82 ┤ ╭──────╯
20.36 ┤ ╭──────╯
13.91 ┤ ╭──────╯
7.45 ┤ ╭──────╯
1.00 ┼──╯
plot data from stdin
..

Acknowledgement
----------------
This package is golang port of library `asciichart <https://github.com/kroitor/asciichart>`_ written by `@kroitor <https://github.com/kroitor>`_.
Expand Down
84 changes: 37 additions & 47 deletions asciigraph.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,46 @@
package asciigraph

import (
"bytes"
"fmt"
"math"
"strings"
)

// Plot returns ascii graph for a series.
func Plot(series []float64, config map[string]interface{}) string {
func Plot(series []float64, options ...Option) string {
config := configure(config{
Offset: 3,
}, options)

var height, offset int
var caption string

if val, ok := config["width"].(int); ok {
series = interpolateArray(series, val)
if config.Width > 0 {
series = interpolateArray(series, config.Width)
}

minimum, maximum := minMaxFloat64Slice(series)

interval := math.Abs(maximum - minimum)

if val, ok := config["height"].(int); ok {
height = val
} else {
if config.Height <= 0 {
if int(interval) <= 0 {
height = int(interval * math.Pow10(int(math.Ceil(-math.Log10(interval)))))
config.Height = int(interval * math.Pow10(int(math.Ceil(-math.Log10(interval)))))
} else {
height = int(interval)
config.Height = int(interval)
}
}

if val, ok := config["offset"].(int); ok {
offset = val
} else {
offset = 3
}

if val, ok := config["caption"].(string); ok {
caption = val
} else {
caption = ""
if config.Offset <= 0 {
config.Offset = 3
}

ratio := float64(height) / interval

ratio := float64(config.Height) / interval
min2 := math.Floor(minimum * ratio)
max2 := math.Ceil(maximum * ratio)

intmin2 := int(min2)
intmax2 := int(max2)

rows := int(math.Abs(float64(intmax2 - intmin2)))
width := len(series) + offset
width := len(series) + config.Offset

var plot [][]string

Expand All @@ -75,74 +64,75 @@ func Plot(series []float64, config map[string]interface{}) string {
} 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(offset)-float64(len(label)), 0))
h := int(math.Max(float64(config.Offset)-float64(len(label)), 0))

plot[w][h] = label
if y == 0 {
plot[w][offset-1] = "┼"
plot[w][config.Offset-1] = "┼"
} else {
plot[w][offset-1] = "┤"
plot[w][config.Offset-1] = "┤"
}
}

y0 := int(series[0]*ratio - min2)

var y1 int

plot[rows-y0][offset-1] = "┼" // first value
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+offset] = "─"
plot[rows-y0][x+config.Offset] = "─"
} else {
if y0 > y1 {
plot[rows-y1][x+offset] = "╰"
plot[rows-y1][x+config.Offset] = "╰"
} else {
plot[rows-y1][x+offset] = "╭"
plot[rows-y1][x+config.Offset] = "╭"
}
if y0 > y1 {
plot[rows-y0][x+offset] = "╮"
plot[rows-y0][x+config.Offset] = "╮"
} else {
plot[rows-y0][x+offset] = "╯"
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+offset] = "│"
plot[rows-y][x+config.Offset] = "│"
}

}

}

var lines []string

// join columns
for _, v := range plot {
lines = append(lines, strings.Join(v, ""))
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 caption != "" {
lines = append(lines, fmt.Sprintf("%s", strings.Repeat(" ", offset+maxWidth+2)+caption))
if config.Caption != "" {
lines.WriteRune('\n')
lines.WriteString(strings.Repeat(" ", config.Offset+maxWidth+2))
lines.WriteString(config.Caption)
}

return strings.Join(lines, "\n") // join rows
return lines.String()
}
93 changes: 51 additions & 42 deletions asciigraph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@ import (
)

func TestPlot(t *testing.T) {
type input struct {
data []float64
conf map[string]interface{}
}

cases := []struct {
in input
want string
data []float64
opts []Option
expected string
}{
{
input{[]float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1}, map[string]interface{}{}},

[]float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1},
nil,
` 11.00 ┤ ╭╮
10.00 ┤ ││
9.00 ┼ ││
Expand All @@ -33,10 +29,8 @@ func TestPlot(t *testing.T) {
-1.00 ┤ ││
-2.00 ┤ ╰╯ `},
{
input{
[]float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 4, 5, 6, 9, 4, 0, 6, 1, 5, 3, 6, 2},
map[string]interface{}{"caption": "Plot using asciigraph."}},

[]float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 4, 5, 6, 9, 4, 0, 6, 1, 5, 3, 6, 2},
[]Option{Caption("Plot using asciigraph.")},
` 11.00 ┤ ╭╮
10.00 ┤ ││
9.00 ┼ ││ ╭╮
Expand All @@ -53,31 +47,25 @@ func TestPlot(t *testing.T) {
-2.00 ┤ ╰╯
Plot using asciigraph.`},
{
input{
[]float64{.2, .1, .2, 2, -.9, .7, .91, .3, .7, .4, .5},
map[string]interface{}{"caption": "Plot using asciigraph."}},

[]float64{.2, .1, .2, 2, -.9, .7, .91, .3, .7, .4, .5},
[]Option{Caption("Plot using asciigraph.")},
` 2.00 ┤
1.03 ┼ ╭╮ ╭╮
0.07 ┼──╯│╭╯╰───
-0.90 ┤ ╰╯
Plot using asciigraph.`},
{
input{
[]float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1},
map[string]interface{}{"height": 4, "offset": 3}},

[]float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1},
[]Option{Height(4), Offset(3)},
` 11.00 ┤
8.40 ┼ ╭╮
5.80 ┤ ╭─╯│╭╮
3.20 ┤╮ ╭╮│ ╰╯│
0.60 ┼╰─╯││ ╰
-2.00 ┤ ╰╯ `},
{
input{
[]float64{.453, .141, .951, .251, .223, .581, .771, .191, .393, .617, .478},
map[string]interface{}{}},

[]float64{.453, .141, .951, .251, .223, .581, .771, .191, .393, .617, .478},
nil,
` 0.95 ┤
0.86 ┤ ╭╮
0.77 ┤ ││ ╭╮
Expand All @@ -90,9 +78,8 @@ func TestPlot(t *testing.T) {
0.14 ┤╰╯ `},

{
input{[]float64{.01, .004, .003, .0042, .0083, .0033, 0.0079},
map[string]interface{}{}},

[]float64{.01, .004, .003, .0042, .0083, .0033, 0.0079},
nil,
` 0.010 ┼╮
0.009 ┤│
0.008 ┤│ ╭╮╭
Expand All @@ -103,10 +90,8 @@ func TestPlot(t *testing.T) {
0.003 ┤ ╰╯ ╰╯ `},

{
input{
[]float64{192, 431, 112, 449, -122, 375, 782, 123, 911, 1711, 172},
map[string]interface{}{"height": 10}},

[]float64{192, 431, 112, 449, -122, 375, 782, 123, 911, 1711, 172},
[]Option{Height(10)},
` 1711 ┤
1544 ┼ ╭╮
1378 ┤ ││
Expand All @@ -120,10 +105,8 @@ func TestPlot(t *testing.T) {
45 ┤ ││
-122 ┤ ╰╯ `},
{
input{
[]float64{0.3189989805, 0.149949026, 0.30142492354, 0.195129182935, 0.3142492354, 0.1674974513, 0.3142492354, 0.1474974513, 0.3047974513},
map[string]interface{}{"width": 30, "height": 5, "caption": "Plot with custom height & width."}},

[]float64{0.3189989805, 0.149949026, 0.30142492354, 0.195129182935, 0.3142492354, 0.1674974513, 0.3142492354, 0.1474974513, 0.3047974513},
[]Option{Width(30), Height(5), Caption("Plot with custom height & width.")},
` 0.32 ┤
0.29 ┼╮ ╭─╮ ╭╮ ╭
0.27 ┤╰╮ ╭─╮ ╭╯ │ ╭╯│ │
Expand All @@ -132,13 +115,39 @@ func TestPlot(t *testing.T) {
0.19 ┤ ╰╮│ ╰╯ ╰╯ │╭╯
0.16 ┤ ╰╯ ╰╯
Plot with custom height & width.`},
{
[]float64{
0, 0, 0, 0, 1.5, 0, 0, -0.5, 9, -3, 0, 0, 1, 2, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1.5, 0, 0, -0.5, 8, -3, 0, 0, 1, 2, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1.5, 0, 0, -0.5, 10, -3, 0, 0, 1, 2, 1, 0, 0, 0, 0,
},
[]Option{Offset(10), Height(10), Caption("I'm a doctor, not an engineer.")},
` 10.00 ┤ ╭╮
8.82 ┤ ╭╮ ││
7.64 ┤ ││ ╭╮ ││
6.45 ┼ ││ ││ ││
5.27 ┤ ││ ││ ││
4.09 ┤ ││ ││ ││
2.91 ┤ ││ ╭╮ ││ ╭╮ ││ ╭╮
1.73 ┤ ╭╮ ││ ╭╯╰╮ ╭╮ ││ ╭╯╰╮ ╭╮ ││ ╭╯╰╮
0.55 ┼───╯╰──╯│╭─╯ ╰───────╯╰──╯│╭─╯ ╰───────╯╰──╯│╭─╯ ╰───
-0.64 ┤ ││ ││ ││
-1.82 ┤ ╰╯ ╰╯ ╰╯
-3.00 ┤
I'm a doctor, not an engineer.`},
}

for _, c := range cases {
got := Plot(c.in.data, c.in.conf)
fmt.Println(got + "\n")
if got != c.want {
t.Errorf("Plot(%f, %q) == %q, want %q", c.in.data, c.in.conf, got, c.want)
}
for i := range cases {
name := fmt.Sprintf("%d", i)
t.Run(name, func(t *testing.T) {
c := cases[i]
actual := Plot(c.data, c.opts...)
if actual != c.expected {
conf := configure(config{}, c.opts)
t.Errorf("Plot(%f, %#v)", c.data, conf)
t.Logf("expected:\n%s\n", c.expected)
}
t.Logf("actual:\n%s\n", actual)
})
}
}

0 comments on commit 2fec436

Please sign in to comment.