route/vendor/github.com/aclements/go-moremath/cmd/dist/plot.go

212 lines
5.1 KiB
Go

// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"io"
"math"
"unicode/utf8"
"github.com/aclements/go-moremath/scale"
"github.com/aclements/go-moremath/stats"
"github.com/aclements/go-moremath/vec"
)
const (
// printSamples is the number of points on the X axis to
// sample a function at for printing.
printSamples = 500
// printWidth is the width of the plot area in dots.
printWidth = 70 * 2
// printHeight is the height of the plot area in dots.
printHeight = 3 * 4
printXMargin = 1
printYMargin = 1
)
// FprintPDF prints a Unicode representation of the PDF of each
// distribution in dists to w. Multiple distributions are printed
// stacked vertically and on the same X axis (but possibly different Y
// axes).
func FprintPDF(w io.Writer, dists ...stats.Dist) error {
xscale, xs := commonScale(dists...)
for _, d := range dists {
if err := fprintFn(w, d.PDF, xscale, xs); err != nil {
return err
}
}
return fprintScale(w, xscale)
}
// FprintCDF is equivalent to FprintPDF, but prints the CDF of each
// distribution.
func FprintCDF(w io.Writer, dists ...stats.Dist) error {
xscale, xs := commonScale(dists...)
for _, d := range dists {
if err := fprintFn(w, d.CDF, xscale, xs); err != nil {
return err
}
}
return fprintScale(w, xscale)
}
// makeScale creates a linear scale from [x1, x2) to [y1, y2).
func makeScale(x1, x2 float64, y1, y2 int) scale.QQ {
return scale.QQ{
Src: &scale.Linear{Min: x1, Max: x2, Clamp: true},
Dest: &scale.Linear{Min: float64(y1), Max: float64(y2) - 1e-10},
}
}
func commonScale(dist ...stats.Dist) (xscale scale.QQ, xs []float64) {
var l, h float64
if len(dist) == 0 {
l, h = -1, 1
} else {
l, h = dist[0].Bounds()
for _, d := range dist[1:] {
dl, dh := d.Bounds()
l, h = math.Min(l, dl), math.Max(h, dh)
}
}
xscale = makeScale(l, h, printXMargin, printWidth-printXMargin)
//xscale.Src.Nice(10)
src := xscale.Src.(*scale.Linear)
xs = vec.Linspace(src.Min, src.Max, printSamples)
return
}
func fprintScale(w io.Writer, sc scale.QQ) error {
img := make([][]bool, printWidth)
for i := range img {
if i < printXMargin || i >= printWidth-printXMargin {
img[i] = make([]bool, 2)
} else {
img[i] = []bool{true, false}
}
}
major, _ := sc.Src.Ticks(scale.TickOptions{Max: 3})
labels := make([]string, len(major))
lpos := make([]int, len(major))
for i, tick := range major {
x := int(sc.Map(tick))
img[x][1] = true
// TODO: It would be nice if the scale could format
// these ticks in a consistent way.
labels[i] = fmt.Sprintf("%g", tick)
width := len(labels[i])
lpos[i] = minint(maxint(x/2-width/2, 0), (printWidth+1)/2-width)
}
if err := fprintImage(w, img, []string{""}); err != nil {
return err
}
curpos := 0
for i, label := range labels {
gap := lpos[i] - curpos
if i > 0 {
gap = maxint(gap, 1)
}
_, err := fmt.Fprintf(w, "%*s%s", gap, "", label)
if err != nil {
return err
}
curpos += gap + len(label)
}
_, err := fmt.Fprintf(w, "\n")
return err
}
func fprintFn(w io.Writer, fn func(float64) float64, xscale scale.QQ, xs []float64) error {
ys := vec.Map(fn, xs)
yl, yh := stats.Bounds(ys)
if yl > 0 && yl-(yh-yl)*0.1 <= 0 {
yl = 0
}
yscale := makeScale(yh, yl, printYMargin, printHeight-printYMargin)
// Render the function to an image.
img := make([][]bool, printWidth+2)
for i := range img {
img[i] = make([]bool, printHeight)
}
for i, x := range xs {
img[int(xscale.Map(x))][int(yscale.Map(ys[i]))] = true
}
// Render Y axis.
ypos := printWidth
for y := printYMargin; y < printHeight-printYMargin; y++ {
img[ypos][y] = true
}
img[ypos+1][printYMargin] = true
img[ypos+1][len(img[0])-1-printYMargin] = true
trail := make([]string, (printHeight+3)/4)
trail[0] = fmt.Sprintf(" %4.3f", yh)
trail[len(trail)-1] = fmt.Sprintf(" %4.3f", yl)
return fprintImage(w, img, trail)
}
func fprintImage(w io.Writer, img [][]bool, trail []string) error {
var x, y int
bit := func(ox, oy int) byte {
if x+ox < len(img) && y+oy < len(img[x+ox]) && img[x+ox][y+oy] {
return 1
}
return 0
}
maxTrail := len(trail[0])
for _, trail1 := range trail {
maxTrail = maxint(maxTrail, len(trail1))
}
buf := make([]byte, 3*(len(img)+1)/2+maxTrail+1)
for y = 0; y < len(img[0]); y += 4 {
bufpos := 0
for x = 0; x < len(img); x += 2 {
// Grab the 2x4 cell of pixels and encode it
// into a byte with the following bit layout:
// 0 3
// 1 4
// 2 5
// 6 7
cell := bit(0, 0)<<0 | bit(1, 0)<<3
cell |= bit(0, 1)<<1 | bit(1, 1)<<4
cell |= bit(0, 2)<<2 | bit(1, 2)<<5
cell |= bit(0, 3)<<6 | bit(1, 3)<<7
// Translate cell into the Unicode Braille space.
r := 0x2800 + rune(cell)
bufpos += utf8.EncodeRune(buf[bufpos:], r)
}
bufpos += copy(buf[bufpos:], trail[y/4])
buf[bufpos] = '\n'
if _, err := w.Write(buf[:bufpos+1]); err != nil {
return err
}
}
return nil
}
// TODO: These should be exported by go-moremath.
func maxint(a, b int) int {
if a > b {
return a
}
return b
}
func minint(a, b int) int {
if a < b {
return a
}
return b
}