Skip to content

Commit

Permalink
Plot multiple series together
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanBaulch committed Apr 12, 2022
1 parent f5286d3 commit 7afebb5
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 42 deletions.
114 changes: 72 additions & 42 deletions asciigraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,45 @@ import (

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

lenMax := 0
for i := range data {
if l := len(data[i]); l > lenMax {
lenMax = l
}
}

if config.Width > 0 {
series = interpolateArray(series, config.Width)
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 := minMaxFloat64Slice(series)
minimum, maximum := math.Inf(1), math.Inf(-1)
for i := range data {
min, max := minMaxFloat64Slice(data[i])
if min < minimum {
minimum = min
}
if max > maximum {
maximum = max
}
}
interval := math.Abs(maximum - minimum)

if config.Height <= 0 {
Expand Down Expand Up @@ -47,7 +75,7 @@ func Plot(series []float64, options ...Option) string {
intmax2 := int(max2)

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

plot := make([][]string, rows+1)

Expand Down Expand Up @@ -99,52 +127,54 @@ func Plot(series []float64, options ...Option) string {
plot[w][config.Offset-1] = "┤"
}

var y0, y1 int
for i := range data {
series := data[i]
var y0, y1 int

if !math.IsNaN(series[0]) {
y0 = int(round(series[0]*ratio) - min2)
plot[rows-y0][config.Offset-1] = "┼" // first value
}
if !math.IsNaN(series[0]) {
y0 = int(round(series[0]*ratio) - min2)
plot[rows-y0][config.Offset-1] = "┼" // first value
}

for x := 0; x < len(series)-1; x++ { // plot the line
for x := 0; x < len(series)-1; x++ { // plot the line
d0 := series[x]
d1 := series[x+1]

d0 := series[x]
d1 := series[x+1]
if math.IsNaN(d0) && math.IsNaN(d1) {
continue
}

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] = "╴"
continue
}

if math.IsNaN(d1) && !math.IsNaN(d0) {
y0 = int(round(d0*ratio) - float64(intmin2))
plot[rows-y0][x+config.Offset] = ""
continue
}
if math.IsNaN(d0) && !math.IsNaN(d1) {
y1 = int(round(d1*ratio) - float64(intmin2))
plot[rows-y1][x+config.Offset] = ""
continue
}

if math.IsNaN(d0) && !math.IsNaN(d1) {
y0 = int(round(d0*ratio) - float64(intmin2))
y1 = int(round(d1*ratio) - float64(intmin2))
plot[rows-y1][x+config.Offset] = "╶"
continue
}

y0 = int(round(d0*ratio) - float64(intmin2))
y1 = int(round(d1*ratio) - float64(intmin2))

if y0 == y1 {
plot[rows-y0][x+config.Offset] = "─"
} else {
if y0 > y1 {
plot[rows-y1][x+config.Offset] = "╰"
plot[rows-y0][x+config.Offset] = "╮"
if y0 == y1 {
plot[rows-y0][x+config.Offset] = "─"
} else {
plot[rows-y1][x+config.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+config.Offset] = "│"
if y0 > y1 {
plot[rows-y1][x+config.Offset] = "╰"
plot[rows-y0][x+config.Offset] = "╮"
} else {
plot[rows-y1][x+config.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+config.Offset] = "│"
}
}
}
}
Expand Down Expand Up @@ -174,8 +204,8 @@ func Plot(series []float64, options ...Option) string {
if config.Caption != "" {
lines.WriteRune('\n')
lines.WriteString(strings.Repeat(" ", config.Offset+maxWidth))
if len(config.Caption) < len(series) {
lines.WriteString(strings.Repeat(" ", (len(series)-len(config.Caption))/2))
if len(config.Caption) < lenMax {
lines.WriteString(strings.Repeat(" ", (lenMax-len(config.Caption))/2))
}
lines.WriteString(config.Caption)
}
Expand Down
55 changes: 55 additions & 0 deletions asciigraph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,58 @@ func TestPlot(t *testing.T) {
})
}
}

func TestPlotMany(t *testing.T) {
cases := []struct {
data [][]float64
opts []Option
expected string
}{
{
[][]float64{{0}, {1}, {2}},
nil,
`
2.00 ┼
1.00 ┼
0.00 ┼`},
{
[][]float64{{0, 0, 2, 2, math.NaN()}, {1, 1, 1, 1, 1, 1, 1}, {math.NaN(), math.NaN(), math.NaN(), 0, 0, 2, 2}},
nil,
`
2.00 ┤ ╭─╴╭─
1.00 ┼────│─
0.00 ┼─╯╶─╯`},
{
[][]float64{{0, 0, 0}, {math.NaN(), 0, 0}, {math.NaN(), math.NaN(), 0}},
nil,
` 0.00 ┼╶╶`},
{
[][]float64{{0, 1, 0}, {2, 3, 4, 3, 2}, {4, 5, 6, 7, 6, 5, 4}},
[]Option{Width(21), Caption("interpolation test")},
`
7.00 ┤ ╭──╮
6.00 ┤ ╭───╯ ╰───╮
5.00 ┤ ╭──╯ ╰──╮
4.00 ┼─╯ ╭───╮ ╰─
3.00 ┤ ╭──╯ ╰──╮
2.00 ┼─╯ ╰─╴
1.00 ┤ ╭───╮
0.00 ┼─╯ ╰╴
interpolation test`},
}

for i := range cases {
name := fmt.Sprintf("%d", i)
t.Run(name, func(t *testing.T) {
c := cases[i]
expected := strings.TrimPrefix(c.expected, "\n")
actual := PlotMany(c.data, c.opts...)
if actual != expected {
conf := configure(config{}, c.opts)
t.Errorf("Plot(%f, %#v)", c.data, conf)
t.Logf("expected:\n%s\n", expected)
}
t.Logf("actual:\n%s\n", actual)
})
}
}
48 changes: 48 additions & 0 deletions examples/rainbow/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"fmt"
"math"

"github.com/guptarohit/asciigraph"
)

func main() {
data := make([][]float64, 6)

// concentric semi-circles
for i := 0; i < 6; i++ {
for x := -40; x <= 40; x++ {
v := math.NaN()
if r := 40 - i; x >= -r && x <= r {
v = math.Sqrt(math.Pow(float64(r), 2)-math.Pow(float64(x), 2)) / 2
}
data[i] = append(data[i], v)
}
}
graph := asciigraph.PlotMany(data, asciigraph.Precision(0))

fmt.Println(graph)
// Output:
// 20 ┤ ╭───────╭╮───────╮
// 19 ┤ ╭──╭───╭───────╭╮───────╮───╮──╮
// 18 ┤ ╭─╭──╭─╭───╭───────╭╮───────╮───╮─╮──╮─╮
// 17 ┤ ╭─╭─╭─╭─╭──╭──────────╯╰──────────╮──╮─╮─╮─╮─╮
// 16 ┤ ╭─╭─╭╭─╭─╭────╯ ╰────╮─╮─╮╮─╮─╮
// 15 ┤ ╭╭─╭─╭╭─╭──╯ ╰──╮─╮╮─╮─╮╮
// 14 ┤ ╭╭─╭╭─╭╭──╯ ╰──╮╮─╮╮─╮╮
// 13 ┤ ╭─╭╭╭─╭╭─╯ ╰─╮╮─╮╮╮─╮
// 12 ┤ ╭╭╭─╭╭╭─╯ ╰─╮╮╮─╮╮╮
// 11 ┤ ╭─╭╭╭╭╭─╯ ╰─╮╮╮╮╮─╮
// 10 ┤ ╭╭─╭╭╭╭╯ ╰╮╮╮╮─╮╮
// 9 ┤ ╭╭╯╭╭╭╭╯ ╰╮╮╮╮╰╮╮
// 8 ┤ ╭╭╯╭╭╭╭╯ ╰╮╮╮╮╰╮╮
// 7 ┤ ││╭╭╭╭╯ ╰╮╮╮╮││
// 6 ┤ ╭╭╭╭╭╭╯ ╰╮╮╮╮╮╮
// 5 ┤ ││││││ ││││││
// 4 ┤╭╭╭╭╭╭╯ ╰╮╮╮╮╮╮
// 3 ┤││││││ ││││││
// 2 ┤││││││ ││││││
// 1 ┤││││││ ││││││
// 0 ┼╶╶╶╶╶╯ ╰╴╴╴╴╴
}
File renamed without changes.

0 comments on commit 7afebb5

Please sign in to comment.