pax_global_header00006660000000000000000000000064135316535460014525gustar00rootroot0000000000000052 comment=984ba80e42d55e2ef2b851794476ee5286fb53e5 thist-1.0.0/000077500000000000000000000000001353165354600126565ustar00rootroot00000000000000thist-1.0.0/.gitignore000066400000000000000000000000001353165354600146340ustar00rootroot00000000000000thist-1.0.0/LICENSE000066400000000000000000000020661353165354600136670ustar00rootroot00000000000000The MIT License (MIT) Copyright © 2019 Botond Sipos Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. thist-1.0.0/README.md000066400000000000000000000030541353165354600141370ustar00rootroot00000000000000thist - a go package for calculating online histograms with plotting to the terminal and images =============================================================================================== [![Documentation](https://godoc.org/github.com/bsipos/thist?status.svg)](http://godoc.org/github.com/bsipos/thist) [![Go Report Card](https://goreportcard.com/badge/github.com/bsipos/thist)](https://goreportcard.com/report/github.com/bsipos/thist) Example ------- ```go package main import ( "fmt" "github.com/bsipos/thist" "math/rand" "time" ) // randStream return a channel filled with endless normal random values func randStream() chan float64 { c := make(chan float64) go func() { for { c <- rand.NormFloat64() } }() return c } func main() { // create new histogram h := thist.NewHist(nil, "Example histogram", "auto", -1, true) c := randStream() i := 0 for { // add data point to hsitogram h.Update(<-c) if i%50 == 0 { // draw histogram fmt.Println(h.Draw()) time.Sleep(time.Second) } i++ } } ``` [![demo video](http://img.youtube.com/vi/7mrs1QGDyys/0.jpg)](http://www.youtube.com/watch?v=7mrs1QGDyys) TODO ---- - Add more details on online histogram generation. - Add separate object for online estimation of moments. - Maybe add tcell as a back-end? thist-1.0.0/bar.go000066400000000000000000000035631353165354600137600ustar00rootroot00000000000000// Copyright © 2019 Botond Sipos // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package thist // BarSimple plots a bar plot on the terminal. Does not use unicode charcters. func BarSimple(x, y []float64, xlab, ylab []string, title string, info []string) string { if len(xlab) == 0 { xlab = AutoLabel(x, mean(absFloats(x))) } if len(ylab) == 0 { ylab = AutoLabel(y, mean(absFloats(y))) } return Plot(x, y, xlab, ylab, title, info, "#", "@", " ", "_", "|", "-", "|") } // Bar plots a bar plot on the terminal. It makes use of unicode characters. func Bar(x, y []float64, xlab, ylab []string, title string, info []string) string { if len(xlab) == 0 { xlab = AutoLabel(x, mean(absFloats(x))) } if len(ylab) == 0 { ylab = AutoLabel(y, mean(absFloats(y))) } return Plot(x, y, xlab, ylab, title, info, "\u2588", "\u2591", " ", "_", "\u2502", "\u2500", "\u2524") } thist-1.0.0/examples/000077500000000000000000000000001353165354600144745ustar00rootroot00000000000000thist-1.0.0/examples/basic.go000066400000000000000000000010751353165354600161070ustar00rootroot00000000000000package main import ( "fmt" "github.com/bsipos/thist" "math/rand" "time" ) // randStream return a channel filled with endless normal random values func randStream() chan float64 { c := make(chan float64) go func() { for { c <- rand.NormFloat64() } }() return c } func main() { // create new histogram h := thist.NewHist(nil, "Example histogram", "auto", -1, true) c := randStream() i := 0 for { // add data point to hsitogram h.Update(<-c) if i%50 == 0 { // draw histogram fmt.Println(h.Draw()) time.Sleep(time.Second) } i++ } } thist-1.0.0/go.mod000066400000000000000000000006161353165354600137670ustar00rootroot00000000000000module github.com/bsipos/thist go 1.12 require ( golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 golang.org/x/net v0.0.0-20190603091049-60506f45cf65 // indirect golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect golang.org/x/text v0.3.2 // indirect golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468 // indirect gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b ) thist-1.0.0/go.sum000066400000000000000000000070131353165354600140120ustar00rootroot00000000000000github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/wayneashleyberry/terminal-dimensions v1.0.0 h1:LawtS1nqKjAfqrmKOzkcrDLAjSzh38lEhC401JPjQVA= github.com/wayneashleyberry/terminal-dimensions v1.0.0/go.mod h1:PW2XrtV6KmKOPhuf7wbtcmw1/IFnC39mryRET2XbxeE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b h1:Qh4dB5D/WpoUUp3lSod7qgoyEHbDGPUWjIbnqdqqe1k= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= thist-1.0.0/hist.go000066400000000000000000000161671353165354600141670ustar00rootroot00000000000000// Copyright © 2019 Botond Sipos // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package thist import ( "fmt" terminal "golang.org/x/crypto/ssh/terminal" "math" "os" "sort" "strconv" "strings" ) // Hist is a struct holding the parameters and internal state of a histogram object. type Hist struct { Title string BinMode string MaxBins int NrBins int DataCount int DataMap map[float64]float64 DataMin float64 DataMax float64 DataMean float64 DataSd float64 Normalize bool BinStart []float64 BinEnd []float64 Counts []float64 m float64 MaxPrecision float64 Precision float64 BinWidth float64 Info string } // NewHist initilizes a new histogram object. If data is not nil the data points are processed and the state is updated. func NewHist(data []float64, title, binMode string, maxBins int, normalize bool) *Hist { h := &Hist{title, binMode, maxBins, 0, 0, make(map[float64]float64), math.NaN(), math.NaN(), math.NaN(), math.NaN(), normalize, []float64{}, []float64{}, []float64{}, math.NaN(), 14.0, 14.0, math.NaN(), ""} if h.BinMode == "" { h.BinMode = "termfit" } if len(data) > 0 { min, max := data[0], data[0] h.DataMean = data[0] h.DataSd = 0.0 h.m = 0.0 for _, d := range data { if d < min { min = d } if d > max { max = d } h.DataCount++ h.updateMoments(d) } h.DataMin = min h.DataMax = max h.BinStart, h.BinEnd, h.BinWidth = h.buildBins() h.updatePrecision() h.Counts = make([]float64, len(h.BinStart)) for _, v := range data { c := roundFloat64(v, h.Precision) h.DataMap[c]++ i := sort.SearchFloat64s(h.BinStart, c) - 1 if i < 0 { i = 0 } h.Counts[i]++ } h.updateInfo() } return h } // updateInfo updates the info string based on the current internal state. func (h *Hist) updateInfo() { digits := strconv.Itoa(int(h.Precision)) h.Info = fmt.Sprintf("Count: %d Mean: %."+digits+"f Stdev: %."+digits+"f Min: %."+digits+"f Max: %."+digits+"f Precision: %.0f Bins: %d\n", h.DataCount, h.DataMean, h.DataSd, h.DataMin, h.DataMax, h.Precision, len(h.BinStart)) } func (h *Hist) buildBins() ([]float64, []float64, float64) { var n int var w float64 if h.DataMin == h.DataMax { n = 1 w = 1 } else if h.BinMode == "fixed" { n = h.MaxBins w = (h.DataMax - h.DataMin) / float64(n) } else if h.BinMode == "auto" || h.BinMode == "fit" || h.BinMode == "termfit" { w = scottsRule(h.DataCount, h.DataSd) n = int((h.DataMax - h.DataMin) / w) if n < 1 { n = 1 } if h.BinMode == "fit" && n > h.MaxBins { n = h.MaxBins } if h.BinMode == "termfit" { tm, _, terr := terminal.GetSize(int(os.Stderr.Fd())) if terr != nil { tm = 80 } tm -= 10 if n > int(tm) { n = int(tm) } } w = (h.DataMax - h.DataMin) / float64(n) } s := make([]float64, n) e := make([]float64, n) for i := 0; i < n; i++ { s[i] = h.DataMin + float64(i)*w e[i] = h.DataMin + float64(i+1)*w } return s, e, w } // NormCounts returns the normalised counts for each bin. func (h *Hist) NormCounts() []float64 { res := make([]float64, len(h.Counts)) for i, c := range h.Counts { res[i] = c / float64(h.DataCount) / h.BinWidth } return res } // updateMoments calculates the new mean and sd of the dataset after adding a new data point p. func (h *Hist) updateMoments(p float64) { oldMean := h.DataMean h.DataMean += (p - h.DataMean) / float64(h.DataCount) h.m += (p - oldMean) * (p - h.DataMean) h.DataSd = math.Sqrt(h.m / float64(h.DataCount)) } // scottsRule calculates the number of histogram bins based on Scott's rule: // https://en.wikipedia.org/wiki/Histogram#Scott's_normal_reference_rule func scottsRule(n int, sd float64) float64 { h := (3.5 * sd) / math.Pow(float64(n), 1.0/3.0) return h } // Update adds a new data point and updates internal state. func (h *Hist) Update(p float64) { h.DataCount++ oldMin := h.DataMin oldMax := h.DataMax if math.IsNaN(h.DataMin) || p < h.DataMin { h.DataMin = p } if math.IsNaN(h.DataMax) || p > h.DataMax { h.DataMax = p } if h.DataCount == 1 { h.DataMean = p h.DataSd = 0.0 h.m = 0.0 h.BinStart, h.BinEnd, h.BinWidth = h.buildBins() h.updatePrecision() h.Counts = []float64{1.0} } else { h.updateMoments(p) h.updateInfo() } h.DataMap[roundFloat64(p, h.Precision)]++ if !math.IsNaN(oldMin) && p >= oldMin && !math.IsNaN(oldMax) && p <= oldMax { var i int if p == oldMin { i = 0 } else if p == oldMax { i = len(h.Counts) - 1 } else { i = sort.SearchFloat64s(h.BinStart, p) - 1 if i < 0 { i = 0 } } h.Counts[i]++ return } h.BinStart, h.BinEnd, h.BinWidth = h.buildBins() h.updatePrecision() newCounts := make([]float64, len(h.BinStart)) for v, c := range h.DataMap { i := sort.SearchFloat64s(h.BinStart, v) - 1 if i < 0 { i = 0 } newCounts[i] += c } h.Counts = newCounts } // updatePrecision claculates the precision to use for binnig based on the // bin width and the maximum allowed precision. func (h *Hist) updatePrecision() { h.Precision = math.Ceil(-math.Log10(h.BinWidth)) * 2.0 if h.Precision > h.MaxPrecision { h.Precision = h.MaxPrecision } if h.Precision < 1.0 { h.Precision = 1.0 } } // Draw calls Bar to draw the hsitogram to the terminal. func (h *Hist) Draw() string { d := h.Counts if h.Normalize { d = h.NormCounts() } return Bar(h.BinStart, d, []string{}, []string{}, h.Title, strings.Split(strings.TrimRight(h.Info, "\n"), "\n")) } // DrawSimple calls BarSimple to draw the hsitogram to the terminal. func (h *Hist) DrawSimple() string { d := h.Counts if h.Normalize { d = h.NormCounts() } return BarSimple(h.BinStart, d, []string{}, []string{}, h.Title, strings.Split(strings.TrimRight(h.Info, "\n"), "\n")) } // Summary return a string summary of the internal state of a Hist object. func (h *Hist) Summary() string { res := "" // FIXME: TODO return res } // Dump prints the bins and counts to the standard output. func (h *Hist) Dump() string { res := "Bin\tBinStart\tBinEnd\tCount\n" for i, c := range h.Counts { res += fmt.Sprintf("%d\t%.4f\t%.4f\t%.0f\n", i, h.BinStart[i], h.BinEnd[i], c) } return res } thist-1.0.0/img.go000066400000000000000000000040521353165354600137620ustar00rootroot00000000000000// Copyright © 2019 Botond Sipos // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package thist import ( "gonum.org/v1/plot" "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/plotutil" "gonum.org/v1/plot/vg" ) // SaveImage saves a histogram to an image file using gonum plot. func (h *Hist) SaveImage(f string) { data := plotter.Values(h.Counts) if h.Normalize { data = h.NormCounts() } p, err := plot.New() if err != nil { panic(err) } p.Title.Text = h.Title p.Y.Label.Text = "Count" if h.Normalize { p.Y.Label.Text = "Frequency" } bins := make([]plotter.HistogramBin, len(h.BinStart)) for i, binStart := range h.BinStart { bins[i] = plotter.HistogramBin{binStart, h.BinEnd[i], data[i]} } ph := &plotter.Histogram{ Bins: bins, Width: h.DataMax - h.DataMin, FillColor: plotutil.Color(2), LineStyle: plotter.DefaultLineStyle, } ph.LineStyle.Width = vg.Length(0.5) ph.Color = plotutil.Color(0) p.Add(ph) p.X.Label.Text = h.Info if err := p.Save(11.69*vg.Inch, 8.27*vg.Inch, f); err != nil { panic(err) } } thist-1.0.0/plot.go000066400000000000000000000074351353165354600141740ustar00rootroot00000000000000// Copyright © 2019 Botond Sipos // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package thist import ( "fmt" terminal "golang.org/x/crypto/ssh/terminal" "os" "strconv" "strings" ) // Plot is a general plotting function for bar plots. It is used by Bar and BarSimple. func Plot(x, y []float64, xlab, ylab []string, title string, info []string, symbol, negSymbol, space, top, vbar, hbar, tvbar string) string { if len(x) == 0 { return "" } // Based on: http://pyinsci.blogspot.com/2009/10/ascii-histograms.html width, height, terr := terminal.GetSize(int(os.Stderr.Fd())) if terr != nil { width = 80 height = 24 } xll := stringsMaxLen(xlab) yll := stringsMaxLen(ylab) width -= yll + 1 res := strings.Repeat(space, yll+1) + centerPad2Len(title, space, int(width)) + "\n" height -= 4 height -= len(info) height -= xll + 1 xf := xFactor(len(x), int(width)) if xf < 1 { xf = 1 } if xll < xf-2 { height += xll - 1 } ny := normalizeY(y, int(height)) block := strings.Repeat(symbol, xf) nblock := strings.Repeat(negSymbol, xf) if xf > 2 { block = vbar + strings.Repeat(symbol, xf-2) + vbar nblock = vbar + strings.Repeat(negSymbol, xf-2) + vbar } blank := strings.Repeat(space, xf) topBar := strings.Repeat(top, xf) for l := int(height); l > 0; l-- { if yll > 0 { found := false for i, t := range ny { if l == t { res += fmt.Sprintf("%-"+strconv.Itoa(yll)+"s"+tvbar, ylab[i]) found = true break } } if !found { res += strings.Repeat(space, yll) + vbar } } for _, c := range ny { if l == abs(c) { res += topBar } else if l < abs(c) { if c < 0 { res += nblock } else { res += block } } else { res += blank } } res += "\n" } if xll > 0 { res += strings.Repeat(space, yll) + vbar + strings.Repeat(hbar, int(width)) + "\n" if xll < xf-2 { res += strings.Repeat(space, yll) + vbar for _, xl := range xlab { res += vbar + rightPad2Len(xl, space, xf-1) } } else { for i := 0; i < xll; i++ { res += strings.Repeat(space, yll) + vbar for j := yll + 1; j < int(width); j++ { if (j-yll-1)%xf == 0 { bin := (j - yll - 1) / xf if bin < len(xlab) && i < len(xlab[bin]) { res += string(xlab[bin][i]) } else { res += space } } else { res += space } } res += "\n" } } } for _, il := range info { res += strings.Repeat(space, yll) + vbar + centerPad2Len(il, space, int(width)) + "\n" } return res } // normalizeY normalizes y values to a maximum height. func normalizeY(y []float64, height int) []int { max := max(y) res := make([]int, len(y)) for i, x := range y { res[i] = int(x / max * float64(height)) } return res } // xFactor. func xFactor(n int, width int) int { return int(width / n) } thist-1.0.0/util.go000066400000000000000000000111001353165354600141530ustar00rootroot00000000000000// Copyright © 2019 Botond Sipos // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package thist import ( "fmt" "math" "strconv" "strings" ) // Max calculates the maximum of a float64 slice. func max(s []float64) float64 { if len(s) == 0 { return math.NaN() } max := s[0] for _, x := range s { if x > max { max = x } } return max } // Min calculates the minimum of a float64 slice. func min(s []float64) float64 { if len(s) == 0 { return math.NaN() } max := s[0] for _, x := range s { if x < max { max = x } } return max } // Mean calculates the mean of a float64 slice. func mean(s []float64) float64 { if len(s) == 0 { return math.NaN() } var sum float64 for _, x := range s { sum += x } return sum / float64(len(s)) } // AbsFloats calculates the absolute value of a float64 slice. func absFloats(s []float64) []float64 { res := make([]float64, len(s)) for i, x := range s { res[i] = math.Abs(x) } return res } // Abs calculates the absolute value of an integer. func abs(n int) int { if n < 0 { return -n } return n } // ClearScreen uses control characters to clear terminal. func ClearScreen() { fmt.Printf("\033[2J") } // ClearScreen return the control characters to clear terminal. func ClearScreenString() string { return "\033[2J" } // StringsMaxLen returns the length of a longest string in a slice. func stringsMaxLen(s []string) int { if len(s) == 0 { return 0 } max := len(s[0]) for _, x := range s { if len(x) > max { max = len(x) } } return max } // AutoLabel generates automatic labeling based on heuristics-based rounding of the values in s. func AutoLabel(s []float64, m float64) []string { res := make([]string, len(s)) nf := false var digits int if min(s) < 0 { nf = true } if math.Abs(m) == 0 { digits = 5 } else { digits = -int(math.Log10(math.Abs(m) / 5)) } if math.Abs(m) < 10 && digits < 3 { digits = 3 } if digits <= 0 { digits = 1 } if digits > 8 { digits = 8 } dl := 0 for _, x := range s { dg := digits + int(math.Log10(math.Abs(x)+1.0)) if dg < 0 { dg = 0 } if dg > dl { dl = dg } } for i, x := range s { dg := dl - int(math.Log10(math.Abs(x)+1.0)) if dg < 0 { dg = 0 } f := "%." + strconv.Itoa(dg) + "f" ff := f if nf && !(x < 0) { ff = " " + f } res[i] = fmt.Sprintf(ff, x) } return res } // RoundFloat64 rounds a float value to the given precision. func roundFloat64(f float64, n float64) float64 { if n == 0.0 { return math.Round(f) } factor := math.Pow(10, float64(n)) return math.Round(f*factor) / factor } // LeftPad2Len left pads a string to a given length. // https://github.com/DaddyOh/golang-samples/blob/master/pad.go func leftPad2Len(s string, padStr string, overallLen int) string { var padCountInt = 1 + ((overallLen - len(padStr)) / len(padStr)) var retStr = strings.Repeat(padStr, padCountInt) + s return retStr[(len(retStr) - overallLen):] } // RightPad2Len right pads a string to a given length. // https://github.com/DaddyOh/golang-samples/blob/master/pad.go func rightPad2Len(s string, padStr string, overallLen int) string { var padCountInt = 1 + ((overallLen - len(padStr)) / len(padStr)) var retStr = s + strings.Repeat(padStr, padCountInt) return retStr[:overallLen] } // CenterPad2Len center pads a string to a given length. // https://www.socketloop.com/tutorials/golang-aligning-strings-to-right-left-and-center-with-fill-example func centerPad2Len(s string, fill string, n int) string { if len(s) >= n { return s } div := (n - len(s)) / 2 return strings.Repeat(fill, div) + s + strings.Repeat(fill, div) }